Webhooks

Receive an HTTP POST when the Send API emits an event, instead of polling

Webhooks push events from send.dev to a URL you control. You register an endpoint once, and each matching event is delivered as a signed JSON POST.

Webhook endpoints belong to the do.dev platform rather than to send.dev specifically. That means the endpoint API lives at https://api.do.dev/v1/webhooks (not under /v1/send/), and the same endpoint can receive events from other do.dev services if you subscribe to them. This page covers what matters for email.

Events

EventWhen
send.email.sentThe email was accepted and queued (the same moment the POST /v1/send/emails/send request returns 202).
send.email.deliveredThe recipient's mail server accepted the message.
send.email.bouncedThe recipient's mail server rejected it. Hard bounces are also added to your suppression list.
send.email.complainedThe recipient reported it as spam. The address is suppressed.
send.email.unsubscribedA recipient used the unsubscribe link or one-click header.

Together these cover the whole life of a message, so a webhook consumer no longer needs to poll Get an email for the final status. An event for provider-side failures (send.email.failed) is planned. See Events for every payload.

Create an endpoint

POST/v1/webhooks

Register a URL to receive events. The URL must be https and publicly reachable. Returns the signing secret exactly once.

Requires:send:write

The path is https://api.do.dev/v1/webhooks, with no /send segment (https://api.send.dev/v1/webhooks is the equivalent alias). Use the same do_live_ API key you use for the Send API. Legacy sk_live_ keys can send email but cannot manage webhooks; they receive 403 forbidden here. Create a do_live_ key in the dashboard.

Body parameters

urlstringrequired
Your https endpoint. Private, loopback, and link-local addresses are rejected.
enabled_eventsstring[]required
Event types to receive. Exact names, or a wildcard such as send.email.* or *.
descriptionstring
A note for your own reference.
Create an endpoint
curl -X POST https://api.do.dev/v1/webhooks \
-H "Authorization: Bearer $SEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "url": "https://app.example.com/webhooks/send",
  "enabled_events": ["send.email.*"],
  "description": "Production email events"
}'
Response201
{
"id": "whe_2m8x9q4wzk17c",
"url": "https://app.example.com/webhooks/send",
"enabled_events": ["send.email.*"],
"description": "Production email events",
"signing_secret": "whsec_...",
"is_active": true
}

Save the signing secret

signing_secret is returned only in this response. You need it to verify signatures. If you lose it, create a new endpoint and delete the old one in the dashboard.

Choosing enabled_events

PatternReceives
send.email.sentOnly that event.
send.email.*Every Send email event, including ones added later. This is the recommended choice for an email endpoint: when send.email.failed ships, you start receiving it without changing the endpoint.
send.*Every Send event.
*Every event from every do.dev service on the account.

Manage endpoints

GET/v1/webhooks

List endpoints on the account.

Requires:send:read
{
"endpoints": [
  {
    "id": "whe_2m8x9q4wzk17c",
    "url": "https://app.example.com/webhooks/send",
    "description": "Production email events",
    "enabled_events": ["send.email.*"],
    "is_active": true,
    "is_paused": false,
    "consecutive_failures": 0,
    "last_delivery_at": 1757000001500,
    "last_delivery_status": "success",
    "created_at": 1756900000000,
    "updated_at": 1757000001500
  }
]
}
GET/v1/webhooks/{id}

Return one endpoint.

Requires:send:read
POST/v1/webhooks/{id}/test

Deliver a synthetic platform.test event to the endpoint so you can confirm your handler and signature check work.

Requires:send:write

The test event has type: "platform.test" and livemode: false. Your handler should accept it (return 2xx) and can otherwise ignore it.

PUT/v1/webhooks/{id}

Change the URL, subscribed events, description, or enabled flag of an endpoint. Send only the fields you want to change; at least one is required.

Requires:send:write

Body parameters

urlstring
New https endpoint. Same reachability rules as on create.
enabled_eventsstring[]
Replace the subscribed event list. Must not be empty.
descriptionstring
Replace the description.
enabledboolean
false disables delivery without deleting the endpoint; true re-enables it, including after an automatic pause.
{
"id": "whe_2m8x9q4wzk17c",
"url": "https://app.example.com/webhooks/send",
"description": "Production email events",
"enabled_events": ["send.email.bounced", "send.email.complained", "send.email.unsubscribed"],
"is_active": true,
"is_paused": false,
"consecutive_failures": 0,
"created_at": 1756900000000,
"updated_at": 1757010000000
}

Updating does not rotate the signing secret. A 400 validation_error lists what was wrong (an unreachable or non-https url, an empty enabled_events, a non-boolean enabled); an unknown id is 404 not_found.

DELETE/v1/webhooks/{id}

Remove the endpoint. Events already queued for it are dropped.

Requires:send:write
{
"id": "whe_2m8x9q4wzk17c",
"deleted": true
}

All of these work identically on https://api.send.dev/v1/webhooks/{id}.

Delivery behavior

  • Each event is a POST with a JSON body and the signature headers. The User-Agent is DoDevWebhook/1.0.
  • Your endpoint has 15 seconds to respond. Any 2xx counts as success; anything else, or a timeout, is a failure.
  • Failed deliveries are retried up to 4 more times (5 attempts total), roughly 1 minute, 5 minutes, 30 minutes, and 2 hours after the previous attempt. After the last failure the event is marked expired for that endpoint and not retried.
  • Redirects are not followed. Respond directly from the registered URL.
  • Events can occasionally be delivered more than once, and order is not guaranteed. Use the event id to de-duplicate, and use created rather than arrival order if sequence matters.
  • Endpoints that fail continuously may be paused (is_paused: true with a pause_reason). Fix the endpoint, then re-enable it with PUT /v1/webhooks/{id} and { "enabled": true }, or in the dashboard.

Respond quickly. Acknowledge with 200 as soon as you have durably recorded the event, and do any real work afterwards; a handler that does slow work inline will hit the 15-second timeout and cause retries.