≈ half a day
Webhooks are how settlement, deposits, dividends, and corporate actions reach you. Delivery is at-least-once — the same event can arrive twice and out of order — so a production-grade handler verifies the signature on the raw body, responds fast, deduplicates, and reconciles gaps from the delivery log.
The flow
| Step | Endpoint / Event | What it does |
|---|---|---|
| 1 | POST /webhooks | Register your endpoint URL, the event list, and a signing secret (min 16 chars). |
| 2 | x-mystocks-signature | Verify HMAC-SHA256 of the RAW request body with a constant-time comparison before parsing. |
| 3 | Respond 2xx fast | You have 8 seconds. Acknowledge immediately, process asynchronously (queue/job). |
| 4 | Dedupe + idempotent handler | The same event can be delivered more than once — dedupe on a natural key and make processing idempotent. |
| 5 | POST /webhooks/{id}/test | Fire a test.event delivery to prove reachability and signature handling before go-live. |
| 6 | GET /webhooks/{id}/deliveries | Delivery log with per-attempt status — your source of truth for gaps after all retries fail. |
| 7 | GET /stream?since= | Optional: the SSE stream mirrors every webhook event and replays up to 1 h of backlog for reconciliation. |
Implementation
const crypto = require('crypto');
const express = require('express');
const app = express();
// IMPORTANT: raw body — signature covers the exact bytes
app.post('/webhooks/mystocks', express.raw({ type: '*/*' }), async (req, res) => {
// 2. Verify the signature before doing ANYTHING with the payload
const sig = req.headers['x-mystocks-signature'] ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.MYSTOCKS_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const valid = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!valid) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body);
// 4. Dedupe on a natural key — at-least-once means duplicates WILL happen.
// Use a unique-constrained DB table in production, not an in-memory set.
const dedupeKey = [
req.headers['x-mystocks-event'],
event.data.orderId ?? event.data.transactionId ?? event.data.subAccountId ?? '',
event.timestamp,
].join(':');
const fresh = await db.insertIgnoreDuplicate('webhook_events', { key: dedupeKey, payload: event });
// 3. Acknowledge fast; process async. Never do slow work before responding.
res.status(200).send('ok');
if (fresh) await queue.enqueue('process-mystocks-event', event);
});
// Handler tips: derive state from the PAYLOAD (order status, balances),
// never from arrival order — a retried order.pending can land after order.filled.Common mistakes
Prove it in sandbox first