Webhooks

Create a webhook endpoint

POST
/api/v1/webhooks

Registers an HTTPS endpoint to receive email events, and returns its signing secret.

The secret is shown exactly once. There is no endpoint that returns it again — if you lose it, rotate. Verify every delivery against the RAW request body with any Standard Webhooks library (standardwebhooks on npm/PyPI/Go), or see the receive-webhooks guide.

Authorization

bearerAuth admin
headerAuthorizationBearer <token>

An API key from the dashboard under Settings → API keys, sent as Authorization: Bearer aem_….

Authorization has two independent axes.

The scope is ranked — a key satisfies any requirement at or below its own tier:

  • read — see messages, contacts and metrics. Changes nothing, and cannot send.
  • write — everything read does, plus managing templates, contacts, automations, segments and suppressions. This is what editing a template needs.
  • admin — everything write does, plus sending configuration: domains, senders, webhook registration, kill switch, daily cap, brand.

There is no approve scope. It was a rung once; it is not one now, and a key requested with it is rejected.

The approval grant (can_approve) is a separate boolean, not a rung. Delivering mail to a real inbox needs write and the grant. Keeping them on separate axes is what makes the review gate a control rather than a convention: a key that may propose is not automatically a key that may approve its own proposal.

Give your application the lowest tier that works. Most need write and the grant — the dashboard mints that combination as Send + manage; Manage only is the same rung with the grant withheld.

Scope: admin

Header Parameters

Idempotency-Key?string

Makes a retry safe (1–256 chars, kept 24 hours). The same key with the same body replays the first success with Idempotency-Replay: true; the same key with a different body, or while the first request is still running, is 409 conflict. A failed request releases its key. Use a value tied to the thing you are creating, e.g. onboarding/user_42.

Length1 <= length <= 256

Request Body

application/json
  1. body
url*string

HTTPS endpoint that receives the events. Must resolve to a public address — private ranges, localhost and non-default ports are refused, and the check is repeated before every delivery.

Formaturi
event_types*array<>

Events to receive. Subscribe to webhook.test as well so you can verify the endpoint before real mail goes out.

Items1 <= items <= 32
description?|

Your own label. Never sent to the endpoint.

Response Body

Endpoint created. secret appears in this response only.

application/json
  1. response
id?string
url?string
description?string|null
event_types?array<string>
enabled?boolean
disabled_at?|
Formatdate-time
disabled_reason?|

consecutive_failures (auto-disabled after sustained failure), manual (you disabled it, or the endpoint returned 410 Gone), or url_unsafe (the URL stopped resolving to a public address).

secret_hint?string

Last four characters of the signing secret, so you can tell which one is configured. The full secret is returned ONLY when the endpoint is created or its secret is rotated.

consecutive_failures?integer
last_delivery_at?|
Formatdate-time
last_success_at?|
Formatdate-time
last_error?string|null
created_at?string
Formatdate-time
updated_at?string
Formatdate-time
secret?string

Store this now — it is never returned again.

Example Request
curl -X POST 'https://sendandretain.com/api/v1/webhooks' \  -H 'Content-Type: application/json' \  -d '{  "url": "https://hooks.acme.com/sendandretain",  "event_types": [    "email.delivered",    "email.bounced",    "webhook.test"  ]}'
Example Response
{  "id": "whe_9f2a1c",  "url": "https://hooks.acme.com/sendandretain",  "description": "CRM sync",  "event_types": [    "email.delivered",    "email.bounced"  ],  "enabled": true,  "disabled_at": "2019-08-24T14:15:22Z",  "disabled_reason": "string",  "secret_hint": "whsec_…4f9a",  "consecutive_failures": 0,  "last_delivery_at": "2019-08-24T14:15:22Z",  "last_success_at": "2019-08-24T14:15:22Z",  "last_error": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z",  "secret": "whsec_MfKQ9r8sTn3xW1yZ4bC7dE0fG2hJ5kL8"}