Webhooks
Get a signed call on your server when emails are delivered, bounce, get replies or lose a subscriber.
A webhook endpoint is a URL on your server that Sendocado calls when something happens in a project: a reply arrives, an email bounces, someone unsubscribes.
Add an endpoint
The URL must be https and on the public internet. Pick the event types it gets; the answer carries the signing secret.
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/webhook-endpoints" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.example/webhooks/sendocado",
"event_types": ["reply.received", "email.bounced"]
}'Events
| Type | When |
|---|---|
email.sent |
The provider accepted an email |
email.delivered |
The provider delivered it |
email.delivery_delayed |
Delivery is delayed and still being tried |
email.bounced |
It bounced permanently; the address is now blocked |
email.complained |
The recipient marked it as spam; the address is now blocked |
email.failed |
It could not be sent |
email.opened |
It was opened |
email.clicked |
A link in it was clicked |
reply.received |
A reply arrived |
contact.unsubscribed |
An address unsubscribed |
Every call is a POST with a JSON body { type, created_at, data }:
{
"type": "email.bounced",
"created_at": 1790000240000,
"data": {
"send_id": "thjxjqi3ogz5kok16zv0mwufxbv932by",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"contact_id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
"to": "maya@example.com",
"subject": "Welcome to Acme, Maya",
"source": {
"type": "campaign_step",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"enrollment_id": "j3j4wj99ibag7i1mnbqns6puq80idw37"
},
"detail": "550 5.1.1 The email account that you tried to reach does not exist."
}
}{
"type": "reply.received",
"created_at": 1790010800000,
"data": {
"id": "kdfy6spsc3lkr2aqxv9upctnwlavyf4r",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"send_id": "thjxjqi3ogz5kok16zv0mwufxbv932by",
"contact_id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"broadcast_id": null,
"from_email": "maya@example.com",
"from_name": "Maya Patel",
"subject": "Re: Welcome to Acme, Maya",
"preview": "Thanks! Quick question: can I import from a CSV?",
"is_auto_reply": false,
"is_unread": true,
"created_at": 1790010800000,
"text": "Thanks! Quick question: can I import from a CSV?\n\nMaya",
"html_url": null,
"attachments": []
}
}email.opened adds user_agent, and email.clicked adds url and user_agent. contact.unsubscribed carries email, contact_id, send_id and source.
Verify the signature
Calls follow the Standard Webhooks spec. Each has three headers:
webhook-id: the event id, the same on every retry. Use it to drop repeats.webhook-timestamp: when this attempt was sent, in seconds.webhook-signature:v1,and an HMAC-SHA256 signature of<id>.<timestamp>.<body>with your secret.
Check the signature against the raw body before you trust a call. With the standardwebhooks package for Node:
import { Webhook } from 'standardwebhooks'
const webhook = new Webhook(process.env.SENDOCADO_WEBHOOK_SECRET)
export async function POST(request: Request) {
const body = await request.text()
const event = webhook.verify(body, {
'webhook-id': request.headers.get('webhook-id') ?? '',
'webhook-timestamp': request.headers.get('webhook-timestamp') ?? '',
'webhook-signature': request.headers.get('webhook-signature') ?? '',
})
// event is { type, created_at, data }
return new Response(null, { status: 204 })
}Libraries for Python, Go, Ruby, PHP and more are listed on the Standard Webhooks site.
Retries
- Answer with any 2xx status within 10 seconds. Anything else is a failed attempt.
- A failed event is tried again with growing waits, up to 12 attempts over about a day.
- After 5 events in a row fail every attempt, the endpoint is turned off and you get an email. Turn it back on with an update.
- Every attempt is listed in the endpoint’s deliveries for 30 days, and you can deliver any event again.
Rotate the secret
Rotating gives a new secret that signs every attempt from then on, retries included, so switch your receiver to it at once.
See the Webhooks reference for every route.