Skip to main content

Outbound Webhooks

Outbound Webhooks are currently in beta.

Courier can use webhooks to notify your application when an event happens in your account. Currently, only message events are available for delivery via webhook.

Add a new Webhook Destination

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 and related resource nested under data.

Handle the event

Currently, only message:updated events are supported. In the future, other event types will become available (eg: profile:updated).

Return a 200 response

Send a successful 200 response to Courier as quickly as possible. Write any long-running processes as code that can run asynchronously outside the webhook endpoint.

Verifying signature

Verify the events that Courier sends to your webhook endpoints. Courier can optionally sign the webhook events it sends to your endpoints by including a signature in each event's courier-signature header. This allows you to verify that the events were sent by Courier, not by a third party.

t=1631816343012,signature=33777cdae0468ff0939b3609d02d14e6e80ca093c2ea233455f0767055218875

Courier generates signatures using a hash-based message authentication code (HMAC) with SHA-256.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and the signature corresponds to the signature

Before you can verify signatures, you need to retrieve your endpoint’s secret from your Webhooks settings, by clicking on a webhook configuration.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

The timestamp (as a string) The actual JSON payload (i.e., the request body)

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the Courier webhook secret as the key, and use the payload string as the message.

TypeScript
const examplePayload = {
data: {
enqueued: 1631833955972,
event: 'SNKDF4GZK94M0NHXBJQDF8GAQWM1',
id: '1-6143cf63-4f27670f6304f465462695f2',
providers: [],
recipient: 'c156665c-a76c-4440-9676-f25c1b04ba93',
recipientId: 'c156665c-a76c-4440-9676-f25c1b04ba93',
status: 'ENQUEUED',
},
type: 'message:updated',
};

function parseHeader(header) {
if (typeof header !== 'string') {
return null;
}

return header.split(',')
.reduce(
(accum, item) => {
const kv = item.split('=');

if (kv[0] === 't') {
accum.timestamp = kv[1];
}
if (kv[0] === 'signature') {
accum.signature = kv[1];
}

return accum;
},
{
timestamp: -1,
signature: '',
}
);
}
// headers refers to the request headers from incoming webhook event from Courier
const headerDetails = parseHeader(headers['courier-signature']);

const unfoldedSignature = crypto
.createHmac('sha256', secret)
.update(`${headerDetails.timestamp}.${JSON.stringify(examplePayload)}`, 'utf8')
.digest('hex');
}


const isValid = unfoldedSignature === headerDetails.signature;

Step 4: Compare the signatures

Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use constant-time string comparison to compare the expected signature to each of the received signatures.

Data

message:updated

The data property in the webhook response payload for the message:updated event is identical to the information that is returned from the GET /message/{message_id} endpoint

For instance, when we send an email - it goes from ENQUEUED → SENT → DELIVERED → OPENED.

Here's a rundown for how the payloads would look like -

Example payload for an ENQUEUED event

JSON
{
"data": {
"enqueued": 1630512466717,
"event": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"id": "1-612fa552-15f7d6ba51bf229857c037a7",
"notification": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"providers": [],
"recipient": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"recipientId": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"status": "ENQUEUED"
},
"type": "message:updated"
}

Example payload for SENT event

JSON
{
"data": {
"enqueued": 1630512466717,
"event": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"id": "1-612fa552-15f7d6ba51bf229857c037a7",
"notification": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"providers": [
// provider specific info
],
"recipient": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"recipientId": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"sent": 1630512468691,
"status": "SENT"
},
"type": "message:updated"
}

Example payload for DELIVERED event

JSON
{
"data": {
"delivered": 1630512501708,
"enqueued": 1630512466717,
"event": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"id": "1-612fa552-15f7d6ba51bf229857c037a7",
"notification": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"providers": [
// provider specific info
],
"recipient": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"recipientId": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"sent": 1630512468691,
"status": "DELIVERED"
},
"type": "message:updated"
}

Example payload for OPENED event

JSON
{
"data": {
"delivered": 1630512501708,
"enqueued": 1630512466717,
"event": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"id": "1-612fa552-15f7d6ba51bf229857c037a7",
"notification": "SFTYJKSF0241SVH2TWY97TTFFTQG",
"opened": 1630518873072,
"providers": [
// provider specific info
],
"recipient": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"recipientId": "b19fb0e0-8cd6-4337-b41c-92c780c80d1a",
"sent": 1630512468691,
"status": "OPENED"
},
"type": "message:updated"
}

Example payload for UNROUTABLE event

JSON
{
"data": {
"enqueued": 1644594639213,
"error": "No providers added",
"event": "5QDEPHNXMC49GVP83X69J1SXV7CE",
"id": "1-620685cf-bf64c91e464de93a283cb791",
"notification": "5QDEPHNXMC49GVP83X69J1SXV7CE",
"providers": [],
"reason": "NO_PROVIDERS",
"recipient": "ab666576-ac30-4d5f-9559-29b85e94a8a4",
"recipientId": "ab666576-ac30-4d5f-9559-29b85e94a8a4",
"status": "UNROUTABLE"
},
"type": "message:updated"
}

Example payload for UNDELIVERABLE event

JSON
{
"data": {
"enqueued": 1644595488228,
"error": "Notification opted out by user",
"event": "AVV5FVHYX5MJX3NT634DM3EMAYE0",
"id": "1-62068920-79afc13b939d3b7a4ad1e376",
"notification": "AVV5FVHYX5MJX3NT634DM3EMAYE0",
"providers": [
{
"error": "Notification opted out by user",
"status": "UNDELIVERABLE"
}
],
"reason": "UNSUBSCRIBED",
"recipient": "suhas@courier.com",
"recipientId": "suhas_stable_issue_2",
"status": "UNDELIVERABLE"
},
"type": "message:updated"
}

For the notification template submission workflow, we emit published, submitted and submission_canceled events

notification:submitted

Example

JSON
{
"data": {
"id": "<NOTIFICATION_ID>",
"submission_id": 1620095270807 // submission ID is a timestamp of submission
},
"type": "notification:submitted"
}

notification:submission_canceled

Example

JSON
{
"data": {
"id": "<NOTIFICATION_ID>",
"canceled_at": 1620095280807,
"submission_id": 1620095270807
},
"type": "notification:submission_canceled"
}

notification:published

Example

JSON
{
"data": {
"id": "<NOTIFICATION_ID>",
"published_at": 1620095270807
},
"type": "notification:published"
}
Was this helpful?