Production is locked until a market agreement is recorded

Webhooks and the event envelope

One envelope for every event, a signature you can verify in six lines, ordering per aggregate and a retry schedule you can plan around.

Platform10 min

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.

payment.confirmed
{  "id": "evt_01JBV8ZK4M",  "type": "payment.confirmed",  "livemode": true,  "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"  }}
Envelope fields
FieldWhat it is for
idDe-duplicate on this. A replay keeps the same event id.
typeRouting. Always context.verb.
livemodefalse for sandbox and for test events. Drop them in production handlers.
created_atWhen the fact happened, in the market's offset.
aggregate.versionMonotonic 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
X-PalopGO-Signature: t=1791371010,v1=4f1a9e8c…,v1=0c44b2a1…
verify.ts
import crypto from "node:crypto"; export function verify(rawBody: string, header: string, secret: string) {  // the header carries one t and ONE OR MORE v1: keep every signature.  const pairs = header.split(",").map((p) => p.trim().split("="));  const t = pairs.find(([k]) => k === "t")?.[1];  const signatures = pairs.filter(([k]) => k === "v1").map(([, v]) => v);  if (!t || signatures.length === 0) return false;   if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;   const expected = Buffer.from(    crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"),  );  // timingSafeEqual throws on a length mismatch, so check the length first.  return signatures.some((sig) => {    const given = Buffer.from(sig);    return 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

what arrives at your door
POST /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

Keep going