Skip to main content
Courier sends events to your systems. Add your endpoint URL and Courier will POST to it when something happens in your workspace, such as a message being sent, delivered, opened, or failing, a template being published, or an audience changing. For the opposite direction, where your systems send events to Courier, see Inbound Webhooks.

Adding a New Webhook Destination

  1. Go to Settings > Webhooks, under Developers, and find the Outbound Webhooks section.
  2. Click Add.
  3. Give the destination a name and enter its URL. The endpoint needs to accept a POST request.
Webhooks are scoped to the environment where they are created. A webhook created in the test environment will only fire for test events; it will not receive production events, and vice versa. If you need webhooks in both environments, create separate webhook destinations in each.

Handling Requests from Courier

Read the Event Data

Courier sends the event data in the request body. Each event is structured as an object with a type property and related resource data nested under the data property.

Handle the Event

Handle events based on the type property in the payload. Additional event types may be added in the future, so your handler should gracefully ignore unknown types.

Return a 2xx Response

Courier gives your endpoint 10 seconds to respond. Acknowledge the event first and do the real work afterwards, in a queue or background job, so a slow dependency doesn’t turn into a delivery failure.

Delivery and Retries

Courier retries a delivery when the failure looks temporary: Retries continue for roughly a day before Courier gives up. An event can therefore arrive more than once, so make your handler idempotent. Note that data.id identifies the resource rather than the event, so a single message produces several events that share it.

Verifying Signatures

Set a secret on the webhook and Courier signs every event it sends to that destination, adding a courier-signature header. Verifying it confirms an event came from Courier and not a third party. Without a secret, no header is sent.
t is the time Courier signed the event, in milliseconds since the epoch. signature is an HMAC-SHA256 digest, hex encoded. Get the secret from Webhooks settings by opening the webhook configuration. You need it as the HMAC key.

Step 1: Extract the Timestamp and Signature from the Header

Split the header on , to get a list of elements, then split each element on = to get a prefix and value pair. The value for the prefix t is the timestamp, and signature is the signature.

Step 2: Prepare the signed_payload String

Concatenate the timestamp, a . separator, and the raw request body.
Use the raw body exactly as it arrived, not a parsed object you re-serialize. JSON.stringify on a parsed payload can reorder keys or change whitespace, which produces a different hash and a signature that never matches. Most frameworks need to be configured to keep the raw body, for example express.raw() or Next.js with the body parser disabled.

Step 3: Determine the Expected Signature

Compute an HMAC with the SHA256 hash function, using the webhook secret as the key and the signed_payload string as the message.

Step 4: Compare the Signatures

Compare your computed signature to the one in the header with a constant-time comparison, so a mismatch doesn’t leak information through timing. Check the timestamp separately. Reject events whose timestamp falls outside a tolerance window you choose, which limits how long a captured request stays replayable.

Event Data

message:updated

Each message status change triggers a message:updated webhook event. Delivery statuses like DELIVERED depend on your email provider asynchronously confirming delivery, while engagement statuses like OPENED and CLICKED are tracked by Courier directly and fire as soon as the recipient acts. This means you may receive engagement events before delivery confirmation, or without it arriving at all. For the full status lifecycle diagram and definitions, see Message Statuses. The data property in the webhook response payload for the message:updated event is identical to the information returned from the GET /message/:message_id endpoint. Any metadata associated with the message is included in the data property. This includes:
  • trace_id
  • tags
  • event
  • utm
Timestamp fields accumulate as the message progresses, so a CLICKED event still carries enqueued, sent, delivered, and opened. Here’s a DELIVERED event:
What each status adds to data:

Notification Template Events

Courier emits three events for the template submission workflow. Use them to integrate with a translation management system (TMS) or a custom approval workflow.

notification:submitted

Fired when a template is submitted for review, meaning it entered an approval workflow and has not been published yet.

notification:submission_canceled

Fired when a pending submission is canceled before it is published.

notification:published

Fired when a template is published directly with no approval workflow, or when a submission is approved and auto-published. This is the event to listen for if you want to sync template content to an external system.

audiences:updated

Fired when an audience is created or updated.

audiences:user:matched

Fired when a user starts matching an audience, which usually happens when the user is created or updated. If a user’s profile matches the audience’s filters, the user is matched to the audience. This is the same event behind the journey Audience trigger, so you can react to it either with a webhook or by starting a journey.

audiences:user:unmatched

Fired when a user stops matching an audience, which usually happens when the user is removed or updated so that they no longer match the audience’s filters.

audiences:calculated

Fired when Courier finishes recalculating an audience. This background process runs every time you create or update an audience, and how long it takes depends on how many users are in your workspace.