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. This applies to voiceStreamUrl (see Streaming live voice media) as well — there’s no separate “voice streaming” plan capability, it rides on the same voice allowance as voiceUrl/callbackVoiceUrl.

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.

Streaming live voice media

webhookUrls.voiceStreamUrl is a different kind of field from messageUrl/voiceUrl/etc. — instead of an HTTP POST per event, Spenza opens an outbound WebSocket connection to it whenever a live voice call starts on that operator/network:
  • Accepts ws://, wss://, http://, or https:// (Spenza always connects over WebSocket regardless of scheme).
  • On connect, Spenza sends a JSON handshake frame first, then streams binary PCM audio (8kHz, 16-bit, mono) frames in real time for the duration of the call.
  • It’s independent of voiceUrl/callbackVoiceUrl — you can set any combination of the three on a voice/both registration, including voiceStreamUrl alone.
  • It only ever activates if that line’s active plan includes voice support, same as any other voice delivery — see the note above.
  • It’s never used as a POST /api/v1.1/webhooks/{id}/test target — see Testing a registration.
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 (voiceStreamUrl is never a test target — there’s no synthetic way to rehearse the WebSocket relay). 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.