Guides
Webhooks
Receive delivery outcomes on your own server, and verify that they really came from SMSend.
Events
Four event types are published. Two of them fire exactly once and are safe to hang billing reconciliation on; the other two are opt-in.
| Value | Meaning |
|---|---|
MESSAGE_TERMINAL | A message reached a terminal state. Fires exactly once per message, ever. |
BATCH_SETTLED | A batch finished and the money is squared. Fires once per batch, and carries the only refund figure that exists. |
BATCH_ACCEPTED | A batch was accepted, priced and reserved. Fires once per batch, after the transaction commits. |
MESSAGE_STATUS_CHANGED | Every status transition, including intermediate ones. High volume — subscribe deliberately. |
A new endpoint subscribes to MESSAGE_TERMINAL and BATCH_SETTLED unless you say otherwise. MESSAGE_STATUS_CHANGED is excluded from the default on purpose: subscribing to everything by accident multiplies your traffic several times over, and you find out when your endpoint falls over.
The payload
Every event has the same envelope. The id is the delivery's own identifier and matches the delivery header, so you can correlate a retry with the attempt that failed.
Per-message events carry no money
There is deliberately no price or refund field on a message event. Refunds are posted per batch at settlement, later and by a different amount than any single message would imply, so a figure here would have to be invented at a moment when the true one does not yet exist. Reconcile on BATCH_SETTLED.
The settlement event is where the final figures live, including the refund.
Request headers
Each delivery carries the event type, the delivery id and the signature.
Verifying the signature
Every delivery is signed with an HMAC over the timestamp and the raw body, using your endpoint's own secret. Verify it on every request — an unverified webhook endpoint is an unauthenticated write path into your system.
Sign the bytes you received, not your parse of them
Read the raw request body before any JSON parsing, and verify against that. If you re-encode a parsed object, every difference in escaping, key order, float formatting or non-ASCII handling produces a mismatch you cannot debug — and most frameworks re-encode by default.
- Read the raw request body as bytes, before parsing.
- Parse the t and v1 values from the signature header. Reject if either is missing or empty, or if t is not 1 to 12 digits.
- Reject if the timestamp is more than 300 seconds away from your own clock, in either direction. This is what stops a captured request being replayed later.
- Compute the HMAC and compare it in constant time. A plain string comparison leaks the correct signature one byte at a time.
- During a rotation window, accept either the current or the previous secret. SMSend always signs with the newer one.
Ordering
Each delivery carries a sequence number that increases monotonically for your endpoint. The counter is per endpoint rather than global, so it tells you nothing about SMSend's total volume.
Order of arrival is not guaranteed
A retried delivery genuinely arrives after events created later than it. Ignore any event whose sequence is lower than the highest you have already processed for that message — that is the contract, and it is the only thing that makes out-of-order arrival safe.
Delivery and retries
Any 2xx counts as success and nothing else does. Redirects are not followed: a 3xx is recorded as a failure, because following one would let a compromised DNS record silently move your webhook traffic elsewhere.
- Success is
- 2xx
- Request timeout
- 10 s
- Maximum attempts
- 6
- Redirects
- not followed
- Signature tolerance
- ±300 s
- Auto-disable after N consecutive failures
- 20
- Secret rotation grace period
- 24 h
- Delivery history retained
- 30 days
After six attempts the event is gone
A delivery that never succeeds is dropped permanently once the retry budget is spent — it is not queued indefinitely and there is no dead-letter replay. Recover the events you missed from the messages feed, filtered on the moment your endpoint last answered.
Auto-disabling
An endpoint that fails 20 times in a row is switched off and the account is emailed. The counter resets to zero on any success, so it measures a sustained outage rather than accumulated bad luck over months.
SMSend never re-enables an endpoint for you. Fix the endpoint, re-enable it explicitly, and recover the events you missed from the messages feed — nothing that was dropped while it was off is redelivered.
Rotating a secret
Rotating issues a new secret and keeps the old one valid for 24 hours. Both verify during the window while SMSend signs with the new one, so you can deploy at your own pace — but the old secret expires on a clock, and the response tells you the exact deadline you are working to.
Endpoint requirements
The URL must use HTTPS and must not resolve to a private, loopback or link-local address. A hostname whose DNS has not propagated yet is accepted, since a new endpoint is often registered before it is live.