Skip to main content
Spenza can push operator-originated SMS and voice events to a URL you control, instead of you polling for them. You manage this through a REST collection — register a webhook, list your registrations, update or delete them — and every delivery is signed so you can verify it actually came from Spenza.

The event model

You register one webhook per operator + network combination, choosing which event category it covers: There’s at most one registration per (account, operator, network) — eventType isn’t part of that key, so registering a second event type for the same operator+network replaces the first rather than adding a second registration. Registering again for an existing combination without update: true in the body returns 409 CONFLICT.
Registering voice (or both) always succeeds if the operator/network combination itself is valid — there’s no registration-time check that voice is actually enabled the way there is for sms. But you won’t actually receive voice events for a line unless that line’s active plan includes voice support (a plan with no voice allowance leaves voice disabled on the number). Make sure the SIMs you expect voice events for are on a voice-capable plan before assuming a registered voice webhook is misbehaving if nothing arrives.

Registering a webhook

The response echoes your registration back with credential fields masked as "***ENCRYPTED***", plus a one-time signingSecret (format whsec_<64 hex chars>) — see Verifying delivery signatures below. Manage registrations as a normal REST collection:
None of the read/list/get calls ever return signingSecret again — only the create/upsert response does. Store it the moment you get it; losing it means re-registering.

How your endpoint authenticates the caller

Separately from payload signing, authentication (type: basic | bearer | api_key | none) protects your webhook URL — Spenza presents this credential when it calls you, so your endpoint can reject callers that don’t have it. Set it to bearer or basic, not none, so your endpoint isn’t open to anyone who discovers its URL.

Verifying delivery signatures

Every delivery (including test events) carries two headers:
X-Spenza-Timestamp is Unix seconds. The v1 value in X-Spenza-Signature is an HMAC-SHA256 computed over the exact string ${timestamp}.${rawBody}, keyed with your registration’s signingSecret — a Stripe-compatible scheme, chosen so most partners can reuse an existing verifier:
Compare against rawBody (the exact bytes received), not a re-serialized version of the parsed JSON — re-serialization can silently change key order or whitespace and break the comparison. A 5-minute tolerance window on X-Spenza-Timestamp is documented convention (WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300) — Spenza doesn’t enforce it on the sending side; it’s your endpoint’s own replay-protection check to make if you want it.

Retry behavior

Delivery retries are configurable per registration via retryPolicy: A delivery is considered successful when your endpoint returns any 2xx response — respond fast and do your processing after responding, not before. After 10 consecutive delivery failures, a registration’s status automatically flips to suspended; check GET /api/v1.1/webhooks periodically, or the status field on GET /api/v1.1/webhooks/{id}, to catch this.

Testing a registration

Send a one-off synthetic event without waiting for a real operator event:
This sends one attempt (no retries) to whichever URL is configured, in priority order messageUrlcallbackMessageUrlvoiceUrlcallbackVoiceUrl. It’s recorded in the deliveries list but doesn’t count toward the 10-consecutive-failure auto-suspend threshold, so you can rehearse your handler without risking a live registration’s status.

Inspecting and redelivering past events

This never returns the delivered payload, response body, or raw error text — just status/timing metadata (deliveryId, webhookId, eventType, status, responseCode, attempt, deliveredAt). If you find a FAILED delivery worth retrying manually (after fixing your handler, for example), redeliver the original event and body to its original endpoint:
This queues the redelivery and returns 202 immediately with a new PENDING delivery record — it runs on a background queue, not the request cycle, so poll the deliveries list for the outcome.

Idempotency

No delivery/event ID for dedup yet. There’s no dedicated event ID field in the delivered payload itself. Retries (both automatic and manual redelivery) mean a given event may be delivered more than once; design your handler to be safe to run twice — for example, keying off whatever naturally-unique fields the payload contains (message SID, timestamp + from/to number) once you’ve inspected a real payload, and ignoring exact duplicates.

Example payloads

Payload schema is being published. The exact JSON body sent to messageUrl/voiceUrl on a real inbound SMS or voice event isn’t published yet. Until then, register a webhook against a test endpoint (a request-inspection tool) and use POST /api/v1.1/webhooks/{id}/test or a real event to capture the actual shape before writing a parser.

Next steps

  • Security & Best Practices for handling the credentials in authentication and signingSecret.
  • API Reference → Webhooks (sidebar) for the full field-level schema of the registration and delivery objects.