Event types
Subscribe to any subset of these. The outbound lifecycle events fire as a message moves through the pipeline; the inbound event fires when mail arrives at one of your inboxes.delivery
The receiving mail server accepted the message.
bounce
A hard or soft bounce. The address is added to your suppression list.
complaint
The recipient marked it as spam. The address is suppressed.
open
Recipient opened the message (tracking pixel).
click
Recipient clicked a tracked link.
inbound_received
A new email arrived at one of your inboxes.
accepted, queued, sending, sent, delivery_delayed, rejected, rendering_failure, failed, and inbound_rejected. Subscribe only to what you act on — most integrations want delivery, bounce, complaint, and inbound_received.
Subscribe an endpoint
Create a webhook with your endpoint URL and the events it should receive. The response includes asigningSecret — it is returned only once, so store it immediately (you’ll use it to verify deliveries).
POST /v1/webhooks
201 Created
PATCH /v1/webhooks/{id} (drin.webhooks.update(id, { enabled: false })). List and delete with drin.webhooks.list() and drin.webhooks.delete(id).
The delivery payload
Every event is delivered as the same envelope: a deliveryid, the event type, an ISO-8601 createdAt, and a data object. For outbound events data.messageId ties it back to the message; for bounces and complaints data.email is the affected address; for inbound_received the messageId points at the arriving message.
Webhook body
Respond fast, process async. Return
2xx quickly. Do the real work after acknowledging — a slow or failing endpoint is retried with backoff, and persistent failures can disable the webhook. Treat deliveries as at-least-once and dedupe on the envelope id.Verify the signature
Each request carries aDrin-Signature header. It is a Stripe-style scheme: a timestamp and an HMAC-SHA256 of the body, keyed by your endpoint’s signing secret.
Request headers
t— the Unix-seconds timestamp the payload was signed at (equal tocreatedAt).v1— lowercase hexHMAC-SHA256(secret, "<t>.<rawBody>").
<t>.<rawBody> with your signing secret and compare it to v1 in constant time. The SDK does exactly this — including the constant-time compare, the t-vs-createdAt binding, and an optional freshness window — in one call.
Express + drin.webhooks.verify()
drin.webhooks.verify() returns { payload, timestamp } on success and throws DrinWebhookVerificationError on any failure — a tampered body, the wrong secret, a malformed header, a t that disagrees with createdAt, or a delivery outside the toleranceSeconds window. It’s a pure local operation, no network call. For edge runtimes without node:crypto, inject a WebCrypto HMAC via the hmacSha256 option.
Verifying without the SDK
The scheme is simple enough to implement directly in any language — this is the whole of it:Manual verification
End to end
Create the webhook
POST /v1/webhooks with your URL and event types. Save the signingSecret from the response.Capture the raw body
Configure your framework to hand you the unparsed request body (e.g.
express.raw({ type: "application/json" })).Verify, then act
Call
drin.webhooks.verify(rawBody, header, secret). On success, branch on payload.type; on failure, return 400.Test it
Fire a synthetic inbound event with
POST /v1/inbound/simulate — it delivers a real, signed inbound_received webhook flagged test_mode. See Inbound & threads.Receive & reply
The
inbound_received webhook is the front door to the agent inbox loop — receive, read the thread, and reply in one call.