Skip to main content
Webhooks push results to your system, so you do not have to poll for them. Surfais sends a signed HTTPS POST to a URL you register, on the day a scan finishes.
Webhooks are for platform-partner keys only. An organisation key gets 403 partner_only on every webhook route. Every call that creates, changes or deletes something also needs the key’s write scope. Without it the answer is 403 write_scope_required. See Authentication.
You work with three resources:
  • An endpoint is an HTTPS URL of yours, plus the secret Surfais signs its requests with.
  • A subscription tells an endpoint which event type to receive, for which organisations and own brands, and at what threshold.
  • A delivery is one event queued for one endpoint. It keeps the attempt count and the outcome of the last attempt, so you can inspect it and replay it.
There are four event types: scan.completed, score.threshold_crossed, sentiment.threshold_crossed and the webhook.test verification ping.

Set up an endpoint

1

Register an endpoint

Call POST /webhook-endpoints with your receiver’s url and an optional description.
cURL
Replace the URL with your own. It must:
  • use https
  • contain no credentials (no user:password@)
  • resolve to a public address
A URL that breaks a rule is refused with 400 validation_error, and details[].code names the rule: url_invalid (not an absolute URL), url_not_https, url_has_credentials or url_not_public. A private or cloud-metadata address is url_not_public. So is a hostname that does not resolve in time.The body is strict. Any field other than url and description is refused with 400 validation_error, so you cannot choose your own secret.The 201 response carries the endpoint and its signing secret.The secret is whsec_ followed by 43 URL-safe characters: letters, digits, - and _.
The secret is shown once. No later call returns it. Store it in your secret manager before you do anything else. If the response is lost, retry with the same Idempotency-Key to get the same response again. See Idempotency.
2

Subscribe to events

Call POST /webhook-endpoints/{endpointId}/subscriptions once per event type. Send event_type, an optional org_id and brand_id to narrow the scope, and the thresholds the event type takes. This body is strict too.
cURL
Each call answers 201 with the subscription:
You can subscribe before the endpoint is verified. Nothing is sent for a subscription until its endpoint is active. The rules for each event type are in Subscription rules.
3

Verify the endpoint

A new endpoint starts in pending_verification and receives only test pings. Deploy your receiver, make sure it verifies signatures, then ask for a ping:
cURL
The 202 response means the ping is queued, not that it has arrived:
Surfais sends a signed webhook.test event to your URL. When your receiver answers with a 2xx, the endpoint becomes active. Read status on GET /webhook-endpoints/{endpointId} to confirm, or look the ping up in the delivery log.Real events are only queued for active endpoints. A ping that fails leaves the endpoint in pending_verification. Enough failed pings in a row make it failing, like any other endpoint, and the fix is the same: a ping your receiver answers with a 2xx. The failed ping is retried like any other delivery, and every call to POST …/test sends a new ping with its own event id.

Endpoint states

status on an endpoint is one of:
Events from scans that finish while an endpoint is failing are never queued for it, and they are not sent later. After an outage, read what you missed from the API. Start with GET /orgs/{orgId}/brands/summary.
A failing endpoint recovers by itself when one of its queued retries gets a 2xx. To recover sooner than the next retry, or when nothing is left in the queue, send a test ping. GET /webhook-endpoints/{endpointId} also returns consecutive_failures (failed attempts since the last success, across all deliveries to the endpoint), last_success_at and last_failure_at.

Subscription rules

Each event type takes different threshold fields. A body that breaks a rule is refused with 400 validation_error, and details[].code names the rule. webhook.test cannot be subscribed to. It is sent only by POST …/test. min_delta is the smallest move that fires, inclusive, in the subscription’s direction. sentiment_floor fires when mean sentiment falls below the floor. Threshold semantics has the detail.

Scope

  • No org_id: every organisation linked to your partner account. The set is worked out again at each scan, so a newly linked organisation is included and a revoked one stops at once.
  • org_id: one organisation. It must be linked to your partner account. Anything else is 404 not_found. The API never confirms that an organisation exists.
  • brand_id: one own brand of that organisation. An id that is not an own brand of org_id is 404 not_found, and that includes a competitor’s id. brand_id needs org_id. Without it the answer is 400 validation_error with details[].code set to org_id_required.
When several subscriptions on one endpoint match the same event, the endpoint gets one delivery. See One delivery per endpoint.

List and delete subscriptions

GET /webhook-endpoints/{endpointId}/subscriptions lists an endpoint’s subscriptions. DELETE /webhook-endpoints/{endpointId}/subscriptions/{subscriptionId} answers 204. Deliveries already queued from a deleted subscription are kept and still sent. Their subscription_id becomes null.

Rotate the secret

POST /webhook-endpoints/{endpointId}/rotate-secret replaces the secret immediately. The 200 response has the same shape as the 201 above and carries the new secret, shown once.
cURL
Attempts that start after the call are signed with the new secret. An attempt already in flight finishes with the old one.
Always send an Idempotency-Key with this call. If the response is lost and you retry with the same key, you get the first response again, with the same secret. Without a key, every retry rotates again, and a secret you did receive may already have been replaced. See Idempotency.

Change a URL or delete an endpoint

There is no update route. To change a URL, register a new endpoint, give it the same subscriptions, verify it, and then delete the old one. While both are active, each receives its own delivery of every event, with the same event id, so dedupe on the event id. DELETE /webhook-endpoints/{endpointId} answers 204. It is permanent. It removes the endpoint’s subscriptions and its delivery history with it. GET /webhook-endpoints lists your endpoints. It never returns a secret. Another partner’s endpoint, subscription or delivery id is 404 not_found, the same answer as an id that never existed.

Next steps

Verify signatures

The signing algorithm, with complete Node.js and Python receivers.

Webhook events

The envelope, each event’s data, and how thresholds are judged.

Delivery, retries and replay

At-least-once delivery, the retry schedule, the delivery log and replay.

Idempotency

Retry a write safely, including a secret rotation.