At-least-once delivery
Delivery is at-least-once per endpoint. Your receiver can see the same event more than once:- A failed attempt is retried. Every retry carries the same
X-Surfais-Event-Idand the same body bytes. OnlyX-Surfais-TimestampandX-Surfais-Signaturechange, because each attempt is signed when it is sent. - A duplicate can follow a successful attempt too. If Surfais cannot record your
2xx, the delivery is sent again. - A replay sends the same event again on purpose.
data.run_date, then by occurred_at, not by arrival.
So dedupe on X-Surfais-Event-Id. It always equals id in the body. Keep a table keyed on the event id, and let one insert-or-ignore statement decide:
2xx. Any other status tells Surfais the attempt failed, and it is retried.
Retry schedule
There are 9 attempts at most: the first one and 8 retries, spread over just under 8 hours. When attempt 9 fails, the delivery becomes
dead and is not retried again. You can still replay it.
The first 2xx ends the schedule and the delivery becomes succeeded.
What counts as a failure
Only a2xx status is a success. Each of these is a failed attempt, and is retried:
- a
4xxor5xxstatus - any
3xxstatus: Surfais never follows a redirect with a signed body, so point the endpoint URL at its final location - no answer within the attempt timeout
- a connection error
- the endpoint URL no longer passing the registration rules, for example because its hostname now resolves to a private address. The URL is checked again before every attempt.
failing.
The delivery log
GET /webhook-endpoints/{endpointId}/deliveries lists an endpoint’s deliveries, newest first. Add ?status= to narrow it to pending, delivering, succeeded or dead. The list is paginated. Keep the same status while you page: a continuation that names a different one is 400 invalid_cursor.
Values of last_error
Match on the prefix. The text after it carries detail that can change. Other values can appear:
last_error is diagnostic text for people, so treat a value you do not recognise as an unclassified failure.
Replay
POST /webhook-deliveries/{deliveryId}/replay queues a delivery for an immediate new attempt. It works on a delivery that is succeeded, dead or pending, and needs the write scope.
The 202 response:
- The replayed attempt carries the same event id and the same body, with a fresh timestamp and signature.
attemptsis not reset. A replayeddeaddelivery gets one more attempt, and becomesdeadagain if that attempt fails.- A delivery that is being attempted right now answers
409 delivery_in_flight. Wait for it to settle, then replay. - A delivery whose organisation is no longer linked to your partner account answers
409 link_revoked. - An unknown id, or another partner’s, answers
404 not_found.
dead delivery on an endpoint and replays it. It stops paging on has_more: false, waits out a 429 rate_limited, and reports a 409 instead of failing on it.
python replay_dead.py <endpointId> and the Node.js example as node replay_dead.mjs <endpointId>.
Revocation
Your access to an organisation’s events depends on its link to your partner account. The link is checked when a delivery is queued, again when it is taken off the queue, and once more immediately before the request is sent. When a link is revoked, the organisation is deleted, or your partner account is suspended:- no new events are queued for that organisation, from that moment
- deliveries still waiting in the queue are never sent. They become
deadwithlast_errorset tolink_revoked - a replay of any of its deliveries answers
409 link_revoked
Timing
Events are produced when a scan finishes, so they follow each organisation’s scan schedule.- A day with no scan has no events. That is by design.
- A scan you request with
POST /orgs/{orgId}/brands/{brandId}/scan-requestsproduces its events when it completes, like any other scan. See Run a scan. - Each scan event type is produced at most once per own brand per scan date. A second scan on the same date does not send you a second event of a type you were already sent for that brand and date; an event type that did not fire the first time can still fire. The second scan’s results do replace the earlier ones for that date, so the numbers on
GET /orgs/{orgId}/brands/summarycan change with no event. After an on-demand scan on a day that was already scanned, poll instead. See Request a scan.