Skip to Content
WebhooksManaging endpoints

Managing endpoints

Webhook endpoints can be provisioned over the API, for deploy scripts and infrastructure-as-code. For the receiver side, see Webhooks.

Every route on this page, reads included, requires a key with the webhooks:manage scope. It is off by default and cannot be added to an existing key. Without it you get 403 insufficient_scope, and the fix is a new key.

Why reads are gated too: an endpoint is a standing feed of every address you mail, pushed to a URL of the caller’s choosing. That makes endpoint creation the one write in v1 that is an exfiltration primitive rather than a content edit. A key with the base grant can read contacts, but it has to keep asking; a key that can add an endpoint gets them pushed to it forever, silently. The delivery log names the recipient of every event, so it is gated on the same reasoning.

The Webhook endpoint object

{ "id": "whe_2v0dz1k9x4m7", "object": "webhook_endpoint", "url": "https://yourapp.com/webhooks/day3", "description": "production", "events": ["email.bounced", "email.complained", "suppression.created"], "status": "active", "consecutive_failures": 0, "last_success_at": "2026-08-21T09:00:03.000Z", "last_failure_at": null, "last_error": null, "created_at": "2026-08-15T09:00:00.000Z" }

consecutive_failures, last_error and last_failure_at are how you monitor an endpoint’s health. Endpoints are never auto-disabled, so these fields are the signal.

The signing secret is deliberately absent. See below.

List endpoints

GET/v1/webhooksscope: webhooks:manage

Create an endpoint

POST/v1/webhooksscope: webhooks:manage
FieldTypeRequiredNotes
urlstringYesPublic https, port 443 or 8443.
eventsstring[]YesWhich events to send.
descriptionstringNoFor your own bookkeeping.
curl -X POST https://go.day3.app/api/v1/webhooks \ -H "Authorization: Bearer $DAY3_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/webhooks/day3", "description": "production", "events": ["email.bounced", "email.complained", "suppression.created"] }'

The response to this POST is the only time the signing secret is ever returned. Capture it now.

{ "id": "whe_2v0dz1k9x4m7", "object": "webhook_endpoint", "url": "https://yourapp.com/webhooks/day3", "secret": "whsec_...", "events": ["email.bounced", "email.complained", "suppression.created"], "status": "active" }

Retrieve an endpoint

GET/v1/webhooks/{webhook_id}scope: webhooks:manage

Update an endpoint

PATCH/v1/webhooks/{webhook_id}scope: webhooks:manage

url, description, events and status. Changing events replaces the list rather than adding to it.

Delete an endpoint

DELETE/v1/webhooks/{webhook_id}scope: webhooks:manage

Removes the endpoint and its delivery history.

Delivery log

GET/v1/webhooks/{webhook_id}/deliveriesscope: webhooks:manage

Cursor-paginated, newest first. Filter with ?status=.

{ "id": "whd_9x4v0dz1k2m7", "object": "webhook_delivery", "endpoint_id": "whe_2v0dz1k9x4m7", "event_id": "evt_2k4h9x4v0dz1", "event_type": "email.bounced", "status": "failed", "attempt": 3, "response_status": 502, "error": "Bad Gateway", "duration_ms": 10012, "next_attempt_at": "2026-08-21T09:40:00.000Z", "delivered_at": null, "created_at": "2026-08-21T09:00:00.000Z" }

Rows are kept for 30 days.

Note that the signed payload is not here. Over the API it would be a paginated way to read back every event body, and therefore every address you mail. The app’s delivery log view shows it.

The signing secret

Two things stay out of the API on purpose:

  • The signing secret is returned once by POST and has no other representation at any scope. Reveal and rotate live in the app UI behind a session, because a key that could read the secret could forge our events into your own receiver.
  • The signed payload, as above.

If you lost the secret, rotate it in the app under API keys, then Webhooks. See rotating the secret for the zero-downtime order.

Last updated on