Skip to content

Webhooks

Get a signed call on your server when emails are delivered, bounce, get replies or lose a subscriber.

View as Markdown

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.

Add an endpoint
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 }:

email.bounced
{
  "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."
  }
}
reply.received
{
  "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:

Verify a call
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.