Documentation

Webhook endpoints

Subscribe a URL to email events, change or remove the subscription, and see every delivery attempt made to it.

A webhook endpoint is a URL SendRaven posts to when something happens to your mail: a reply arrives, or a message is sent, delivered, bounced, complained about, rejected, opened or clicked. Each delivery is signed with the endpoint's secret, which is returned once, when the endpoint is created. The payloads, the signature header and how to verify it, and the retry schedule are on Webhooks.

When a webhook does not seem to fire, list its delivery attempts: a 500 from your own endpoint looks exactly like no webhook at all from the receiving side.

Event types

EventSent when
inboundA message arrived on one of your domains, usually a reply. See Webhooks for its payload.
sendSES accepted the message for sending.
deliveryThe receiving server accepted it.
bounceThe receiving server rejected it. Permanent bounces also suppress the address.
complaintThe recipient marked it as spam. The address is suppressed.
rejectSES refused it before sending. A send SendRaven skips itself, for example because every recipient is suppressed, produces no event.
openThe recipient opened it. Sent once per message, for the first open only, and not for fetches by link scanners. Recorded only for identities whose link. tracking hostname is active.
clickThe recipient clicked a tracked link. Sent for every click, not only the first, except clicks by link scanners. Recorded only for identities whose link. tracking hostname is active.
rendering_failureThe message could not be rendered.
delivery_delayDelivery failed temporarily and is still being retried.

The webhook endpoint object

Every field is always present. The signing secret is not part of the object: Create a webhook endpoint adds it to its response, once, and no other response carries it.

FieldTypeDescription
idstringUnique identifier (UUID).
urlstringWhere deliveries are posted.
eventsarray of stringThe event types delivered to this endpoint.
enabledbooleanWhether deliveries are made. true on creation.
created_atstring (ISO 8601)When the endpoint was created.

List webhook endpoints

GET /v1/webhook-endpoints · requires webhooks:read

Returns the workspace's endpoints newest first, cursor-paged, without their secrets. See Pagination.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Page size, at most 100. See Pagination.
cursorstringnoneThe next_cursor from the previous page.

Example

curl https://api.sendraven.ai/v1/webhook-endpoints \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c",
      "url": "https://example.com/webhooks/sendraven",
      "events": ["delivery", "bounce", "complaint"],
      "enabled": true,
      "created_at": "2026-09-15T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Create a webhook endpoint

POST /v1/webhook-endpoints · requires webhooks:write

Subscribes a URL to one or more event types. The endpoint is enabled straight away, and its signing secret is in this response and nowhere else: store it before doing anything with the response, because it cannot be read back later and this API has no way to rotate it. To replace a lost secret, create a new endpoint and delete the old one.

Use an https URL. The API also accepts http, but over plain http every payload travels unencrypted. Verify the signature on every delivery either way: an endpoint that does not is one anyone on the internet can post fake events to.

The URL must point at a public address, because deliveries are made from SendRaven's servers. It is refused with 422 when it:

  • uses a scheme other than http or https, or carries a username or password (https://user:pass@example.com);
  • names a port other than 80, 443 or 1024 to 65535. The ports below 1024 belong to services such as SSH and SMTP, not to webhook receivers, and a receiver on 3000, 8080 or 8443 is fine. Leaving the port out uses the scheme's default, which is always allowed. This is answered with its own message, url: port must be 80, 443 or 1024-65535;
  • has a host that is an IP address in a non-public range: loopback (127.0.0.0/8, ::1), unspecified (0.0.0.0, ::), private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7), link-local (169.254.0.0/16, fe80::/10), carrier-grade NAT (100.64.0.0/10), multicast, documentation, benchmarking or reserved. The IPv4-mapped IPv6 form of any of these (::ffff:127.0.0.1) and integer, octal or hex spellings (http://2130706433/) count as the address they stand for;
  • has a host named localhost, internal, local, localdomain or home.arpa, or ending in one of those after a dot (.internal, .local and so on), a single label with no dot (http://metadata/), or a last label that is all digits or 0x followed by hex digits, which is an address written as a name;
  • has a host name that resolves to any address that is not public. Every A and AAAA record is checked, so one private record among public ones is enough. For IPv6, only global unicast addresses (2000::/3) count as public, less the documentation and IETF-reserved ranges inside it.

A host name that does not resolve yet, or whose lookup takes longer than three seconds, is accepted, so you can create the endpoint before its DNS record exists. It is checked again on every delivery.

This response is never stored for idempotency, because it holds the only copy of the secret. A retry with the same Idempotency-Key after this request has answered creates another endpoint, with its own secret; a retry that arrives while it is still running waits up to 8 seconds for it to finish and then creates another endpoint too, because there is no stored response to return; if it is still running after the wait, the retry answers 409 idempotency_in_progress. If a request times out, list the endpoints before retrying and delete any duplicate.

Body

FieldTypeRequiredDefaultDescription
urlstringYesnoneAn absolute http or https URL at a public address, on port 80, 443 or 1024-65535, with no username or password. See the rules above.
eventsarray of stringYesnoneAt least one of inbound, send, delivery, bounce, complaint, reject, open, click, rendering_failure, delivery_delay.

Example

curl https://api.sendraven.ai/v1/webhook-endpoints \
  -X POST \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/webhooks/sendraven",
    "events": ["delivery", "bounce", "complaint"]
  }'

Response

201 Created

A webhook endpoint object with secret added: the signing secret, starting whsec_.

{
  "id": "5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c",
  "url": "https://example.com/webhooks/sendraven",
  "events": ["delivery", "bounce", "complaint"],
  "enabled": true,
  "created_at": "2026-09-15T12:00:00.000Z",
  "secret": "whsec_Qm9yUGx4N2t3ZGh5c2Z1ZWJ0cmFtZG9t"
}

Errors

StatusTypeWhen
422invalid_requesturl is missing or not a URL, or events is missing, empty or contains a value not in the list above. details lists the problems.
422invalid_requesturl names a port other than 80, 443 or 1024-65535. The message is url: port must be 80, 443 or 1024-65535.
422invalid_requesturl is not a public http or https address, as described above. The message is url: must be a public http or https address.

Update a webhook endpoint

PATCH /v1/webhook-endpoints/{id} · requires webhooks:write

Changes an endpoint's URL, events or enabled state. Fields you leave out are unchanged, and unknown fields are ignored. events, when given, replaces the whole list. The secret stays the same. A new url must be a public http or https address, under the same rules as Create a webhook endpoint; a request without url does not re-check the stored one.

Setting enabled to false stops new deliveries to the endpoint. Retries still owed to it are not sent: each time one falls due it is recorded as a failed attempt with the error endpoint removed or disabled. Deliveries that still have attempts left when you set enabled back to true are retried again.

Path parameters

ParameterDescription
idThe endpoint's id.

Body

FieldTypeRequiredDefaultDescription
urlstringNounchangedAn absolute http or https URL at a public address, on port 80, 443 or 1024-65535, with no username or password.
eventsarray of stringNounchangedAt least one event type from the list above. Replaces the current list.
enabledbooleanNounchangedfalse pauses deliveries, true resumes them.

Example

curl https://api.sendraven.ai/v1/webhook-endpoints/5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c \
  -X PATCH \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

Response

200 OK

The webhook endpoint object as stored after the update.

{
  "id": "5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c",
  "url": "https://example.com/webhooks/sendraven",
  "events": ["delivery", "bounce", "complaint"],
  "enabled": false,
  "created_at": "2026-09-15T12:00:00.000Z"
}

Errors

StatusTypeWhen
404not_foundNo endpoint with this id in the workspace. Checked before the body is validated.
422invalid_requesturl is not a URL, events is empty or contains a value not in the list above, or enabled is not a boolean. details lists the problems.
422invalid_requesturl names a port other than 80, 443 or 1024-65535. The message is url: port must be 80, 443 or 1024-65535. Checked after the other validation, and the stored URL is left unchanged.
422invalid_requesturl is not a public http or https address. The message is url: must be a public http or https address. Checked after the other validation and the port, and the stored URL is left unchanged.

Delete a webhook endpoint

DELETE /v1/webhook-endpoints/{id} · requires webhooks:write

Removes the endpoint. No further deliveries are made to it, retries still owed to it are recorded as failed, and its delivery attempts can no longer be listed. To stop deliveries for a while without losing the endpoint and its secret, set enabled to false instead.

Path parameters

ParameterDescription
idThe endpoint's id.

Example

curl https://api.sendraven.ai/v1/webhook-endpoints/5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c \
  -X DELETE \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "id": "5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c",
  "deleted": true
}

Errors

StatusTypeWhen
404not_foundNo endpoint with this id in the workspace.

List delivery attempts

GET /v1/webhook-endpoints/{id}/events · requires webhooks:read

Returns the most recent deliveries made to one endpoint, newest first: one row per event, with the outcome of its latest attempt. This is how to tell "SendRaven never sent it" from "your endpoint returned 500".

The first attempt is made as soon as the event happens, and a failed delivery is retried by a background worker up to six attempts in all. See Webhooks for the schedule.

Every attempt resolves the endpoint's host again and checks each address it resolves to, then connects only to an address it checked. An attempt whose destination is no longer public, for example because the DNS record now points at a private address, is not sent: it is recorded as failed with last_error set to destination resolves to a non-public address, and retried on the usual schedule in case the record is corrected. An endpoint whose URL names a port other than 80, 443 or 1024-65535, saved before that rule existed, is not sent to either: each attempt fails with destination port is not allowed: use 80, 443 or 1024-65535 until the URL is changed. Redirects are not followed: a 3xx answer is a failed attempt, with its status in last_status_code and last_error set to redirect not followed: webhook endpoints must answer directly.

This list is not paged. It returns up to limit rows in the usual list envelope: next_cursor is always null and cursor is ignored. has_more is true when the response holds as many rows as the limit allows (limit, or 200 when limit is higher), which means older deliveries may exist that this endpoint cannot return; it is false otherwise.

Path parameters

ParameterDescription
idThe endpoint's id.

Query parameters

ParameterTypeDefaultDescription
limitinteger50How many recent deliveries to return, at most 200. Zero, a negative number or a value that is not a number falls back to 50.

Example

curl "https://api.sendraven.ai/v1/webhook-endpoints/5a8c1e3f-7b2d-4c6e-9f0a-1b3d5e7f9a2c/events?limit=20" \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "c3e5a7b9-1d2f-4a6c-8e0b-2d4f6a8c0e1b",
      "event": "bounce",
      "status": "failed",
      "attempts": 2,
      "last_status_code": 500,
      "last_error": null,
      "created_at": "2026-09-15T12:00:00.000Z",
      "delivered_at": null
    },
    {
      "id": "9b7d5f3a-2c4e-4d6f-8a1b-3c5e7a9b1d2f",
      "event": "delivery",
      "status": "success",
      "attempts": 1,
      "last_status_code": 200,
      "last_error": null,
      "created_at": "2026-09-15T11:58:00.000Z",
      "delivered_at": "2026-09-15T11:58:01.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
FieldTypeDescription
idstringThe delivery's id (UUID).
eventstringThe event type delivered.
status"pending" | "success" | "failed"pending before the first attempt has finished, success once an attempt got a 2xx, failed after an attempt that did not. A failed delivery with fewer than six attempts is still being retried.
attemptsintegerAttempts made so far.
last_status_codeinteger | nullThe HTTP status your endpoint returned on the most recent attempt that got a response. null if none has.
last_errorstring | nullThe most recent error, up to 500 characters: a timeout or refused connection, destination resolves to a non-public address, destination port is not allowed: use 80, 443 or 1024-65535, could not resolve the destination host, a redirect that was not followed, or endpoint removed or disabled for a retry that fell due while the endpoint was disabled. A later successful attempt does not clear it. Your endpoint answering with any other non-2xx status is recorded in last_status_code only.
created_atstring (ISO 8601)When the event was queued for this endpoint.
delivered_atstring (ISO 8601) | nullWhen an attempt succeeded.

Errors

StatusTypeWhen
404not_foundNo endpoint with this id in the workspace.