Webhooks
Webhooks are fast hints, not the record of truth. The record is the order's append-only event log, and the snapshot you can always pull is the platform's fold of it; a push is just the fastest way to hear that the log grew. The contract is deliberately boring: hints arrive fast, truth is pullable, and nothing depends on your uptime. Design accordingly, on either side of the marketplace.
- Event shapes are a tagged union on
event_type: every lifecycle transition carries the order event envelope (order_id,external_order_id, a per-order strictly increasingseq,status,prior_status, and the payload the transition carries), and each non-lifecycle type carries its own smaller envelope. Retries reuse the sameseq. See the event envelope. - Both directions. One envelope, six types, the same to both sides: every lifecycle transition is
order.status_changed(route onstatusandprior_status), and the five non-lifecycle types cover cards, windows, avails answers, integrity failures, and invoices. See the event types. - Verification: every delivery is HMAC-signed over (timestamp, body) in
X-Signature; verify before trusting, reject stale timestamps. - Delivery is best-effort: 3 attempts with exponential backoff, roughly 25 seconds worst case, then log-and-drop. An outage on your side longer than that misses events, by design; recovery is built in, below.
- No public endpoint? Use a queue. VAMOS can deliver events to a shared SQS queue with AWS-native access control, first-class alongside webhooks. Timestamps are UTC everywhere.
The receiver, correctly
Four duties, in order: verify the signature, acknowledge with a 2xx fast, process async, and dedup on (order_id, seq). Return 204 for duplicates so retries stop.
app.post("/webhooks/vamos", (req, res) => {
verifyHmac(req.headers["x-signature"], req.body); // reject stale timestamps
const { order_id, seq } = req.body;
if (seen(order_id, seq)) return res.status(204).end(); // dedup: retries reuse seq
enqueue(req.body); // ack fast, process async
res.status(200).end();
});
Recovery, always available
- Missed a window?
GET /v1/events?order={id}&after_seq={n}replays everything after your high-water mark on one order, andGET /v1/events/feed?after={cursor}resumes your whole stream at once: everything webhooks would have delivered to you, in order, from wherever you left off. - Not sure where you are?
GET /v1/orders/{id}returns the snapshot withlast_seq; reconcile against it, then replay the gap. - After every deploy or outage: resume the feed from your stored cursor. Without a cursor, sweep
GET /v1/orders?status=submitted,validating,needs_confirmation,confirmed,negotiating,seller_reviewand replay each. Every non-terminal status is in that list; dropneeds_confirmationonly if you never submit document orders. - Long pauses are normal: orders park in
seller_reviewfor minutes or hours with no events. A low-frequency safety poll on parked orders costs nothing and closes the last gap.
The sweep is one half of the reconciliation posture; the rest is Reconciliation.
If you cannot expose a public endpoint at all, request shared-queue delivery (SQS) instead: same envelope, same seq semantics, no inbound firewall conversation.