Webhooks
A webhook is a request we send to your backend when something happens that you couldn’t otherwise know: an email was delivered or bounced, a person left marketing, a workspace’s health moved, your sending domain stopped verifying. Each is signed, so you can check it came from us, and retried until your endpoint takes it. A word on names: you emit events to us, we send webhooks to you, and each webhook has a type.
Endpoints
Section titled “Endpoints”Create a webhook endpoint with a URL and the types it should get:
curl -X POST https://api.sendtruss.com/v1/webhook-endpoints \ -H "Authorization: Bearer $SENDTRUSS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.yourplatform.com/sendtruss", "types": ["message.delivered", "message.bounced", "message.complained", "contact.left_marketing", "contact.suppressed"] }'- The URL must be public HTTPS on port 443, with a host name rather than an IP address, no user or password, and up to 2,048 characters. It must resolve to public addresses only, now and every time we deliver. A URL we refuse answers with a
url_*code that says why, such asurl_not_httpsorurl_address_refused. typeslists at least one type. An unknown type is refused withunknown_type.- Endpoints belong to your platform and get webhooks from every workspace. Each body names its workspace.
- You can have up to 10 endpoints. You can list, read, change and delete them. Webhooks not yet delivered follow a URL change; deleting an endpoint fails the ones it still had.
Your first endpoint also creates your first signing secret. The answer carries it in signing_secret, once and never again, so store it then.
The types
Section titled “The types”| Type | Sent when |
|---|---|
message.delivered |
the recipient’s mail server accepted a transactional email |
message.bounced |
it bounced, with bounce: hard or soft |
message.complained |
the recipient marked it as spam |
message.suppressed |
it wasn’t sent, because the address is suppressed |
message.failed |
it couldn’t be sent, with a reason |
message.expired |
it couldn’t go out within 24 hours |
contact.left_marketing |
a person left marketing through our link or their mail app’s unsubscribe button |
contact.rejoined_marketing |
they undid that within the undo window |
contact.suppressed |
an address a contact holds became suppressed, with reason and what it blocks |
workspace.health_changed |
a workspace’s health moved, with from, to and reason |
sending_domain.changed |
your domain stopped or started being able to send, with each record’s status |
One email can produce more than one outcome: delivered and then complained is normal. The webhooks reference lists every field of every type.
What a webhook looks like
Section titled “What a webhook looks like”A POST with a JSON body:
{ "id": "01JA2QC4M8N2P6R0T4V8X2Z6B0", "type": "message.delivered", "source": "sendtruss", "created_at": "2026-10-08T14:03:15+00:00", "simulated": false, "workspace": {"id": "01JA1ZK7W3X5Y9A2B4C6D8E0F2", "reference": "cust_4821"}, "data": { "message": { "id": "01JA2Q8V7S1A2B3C4D5E6F7G8H", "idempotency_key": "booking-7731-created", "contact": {"id": "01JA2Q5D3B8C1E6F9G2H4J7K0M", "reference": "guest_998"} } }}idis the webhook’s own id. Dedupe on it.created_atis when the thing happened, not when we sent the webhook.sourceis where the change came from:recipientwhen the person did it, such as leaving marketing or marking an email as spam, orsendtrusswhen our sending or our rules recorded it.simulatedis true for the webhooks a simulator address produces.workspaceis null forsending_domain.changed, which belongs to your platform.- A message carries the
idempotency_keyof the emit that sent it, so you can match it to your own record, and itscontact, orrecipient(the address) when it was sent to an address no contact holds. No other body carries an email address.
What gets no webhook
Section titled “What gets no webhook”Changes you made yourself through the API don’t come back as webhooks: a consent you sent, a contact you deleted, an erasure, a workspace you suspended, resumed or deleted. You already know about them, and sending them back would invite a sync loop that reacts to its own echo.
Signatures
Section titled “Signatures”Every webhook carries three headers:
SendTruss-Webhook-Id, the webhook’s id;SendTruss-Webhook-Timestamp, when we signed it, in Unix seconds;SendTruss-Webhook-Signature, one or more<secret id>=<signature>pairs separated by commas.
Each signature is the hex HMAC-SHA256 of the timestamp, a ., and the raw body, keyed with one of your signing secrets. Check it against the raw bytes you received, before any JSON parsing, and refuse a timestamp more than 5 minutes from now. Handle and verify webhooks walks through it.
Signing secrets and rotation
Section titled “Signing secrets and rotation”Your secrets sign the webhooks for all your endpoints. You can hold up to 3. While two or more are live, every webhook carries a signature from each, so you can rotate without dropping one:
- Create a signing secret. The answer shows it once.
- Deploy it, so your verification accepts it.
- Revoke the old one.
List your secrets to see each one’s id and last four characters. You can’t revoke your last secret while you have an endpoint, since nothing could sign what we send it; that’s refused with last_secret_in_use.
Delivery and retries
Section titled “Delivery and retries”Your endpoint must answer with a 2xx within 10 seconds. Anything else counts as a failed attempt: another status, a timeout, a TLS error. A redirect is a failed attempt too, since we don’t follow them. We only keep the status code, never your response body.
After a failure we retry with growing gaps for up to 3 days, then mark the webhook failed. If your endpoint keeps failing, we pause it briefly between attempts, up to an hour, so one dead endpoint doesn’t hold up delivery for everyone; one success brings it back.
Delivery is at least once, and order isn’t guaranteed. You may get the same webhook twice, and a bounce can arrive after a delivery for the same email. Dedupe on id and use created_at for order.
When one didn’t arrive
Section titled “When one didn’t arrive”List webhooks shows every webhook of the last 30 days, newest first, with its state (pending, delivered or failed), its attempts and the last error. Filter by endpoint, state or type. Get a webhook shows its body as we would send it now.
Redeliver a webhook sends it again now, with the same id, and starts a fresh round of retries. Use it after an outage longer than 3 days, or when your side took a webhook and then lost it:
curl -X POST https://api.sendtruss.com/v1/webhooks/01JA2QC4M8N2P6R0T4V8X2Z6B0/redeliver \ -H "Authorization: Bearer $SENDTRUSS_API_KEY"A webhook whose endpoint you deleted can’t be redelivered: that’s refused with endpoint_deleted.