Events are the only supported way to learn that something changed. Polling an order is allowed and rate-limited; it will always be slower and more expensive than being told.
The envelope
Every event has the same five fields at the top and a type-specific data object. The aggregate block is what makes ordering and de-duplication possible on your side.
{ "id": "evt_01JBV8ZK4M", "type": "payment.confirmed", "livemode": , "created_at": "2026-10-07T12:38:02+02:00", "aggregate": { "type": "order", "id": "ord-2026-mz-0476", "version": 4 }, "data": { "market": "mz-mpm", "currency": "MZN", "amount_minor": 39000, "method": "emola", "provider_ref": "EML-2026-100476", "state": "CONFIRMED", "phase": "CONFIRMED" }}| Field | What it is for |
|---|---|
| id | De-duplicate on this. A replay keeps the same event id. |
| type | Routing. Always context.verb. |
| livemode | false for sandbox and for test events. Drop them in production handlers. |
| created_at | When the fact happened, in the market's offset. |
| aggregate.version | Monotonic per aggregate. Ignore anything you have already applied. |
Verify the signature
The signature header carries the timestamp and one or more signatures. Compute the HMAC over the timestamp, a dot and the raw body — not the parsed body, which your framework may have re-serialised.
X-PalopGO-Signature: t=1791371010,v1=4f1a9e8c…,v1=0c44b2a1… crypto "node:crypto"; verify(rawBody: string, header: string, secret: string) { // the header carries one t and ONE OR MORE v1: keep every signature. pairs = header.split(",").map((p) => p.trim().split("=")); t = pairs.find(([k]) => k === "t")?.[1]; signatures = pairs.filter(([k]) => k === "v1").map(([, v]) => v); if (!t || signatures.length === 0) ; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) ; expected = Buffer.( crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), ); // timingSafeEqual throws on a length mismatch, so check the length first. signatures.some((sig) => { given = Buffer.(sig); given.length === expected.length && crypto.timingSafeEqual(expected, given); });}Retries and ordering
- Answer 2xx within 10 seconds. Do the work afterwards; a slow handler reads as a failure.
- Non-2xx and timeouts retry after 1, 5, 25, 120 and 360 minutes, then once a day for three days.
- Delivery is at-least-once. The same event id can arrive twice; an older aggregate version never follows a newer one for the same order.
- After the schedule is exhausted the delivery stays replayable from the portal for 30 days.
- A paused endpoint holds its events for 72 hours and delivers them in order when you resume.
Request headers
/palopgo/payments HTTP/1.1Host: api.bissaufoods.exampleContent-Type: application/jsonX-PalopGO-Event: payment.confirmedX-PalopGO-Delivery: dlv_6b31c0a4X-PalopGO-Attempt: 3X-PalopGO-Signature: t=1791371010,v1=4f1a9e…User-Agent: PalopGO-Webhooks/1