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
- Visit https://app.courier.com/settings/webhooks
- Click
**Add Webhook**
- Enter new Webhook destination information (this should accept a
POST
request)
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.
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
{
"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
{
"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
{
"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
{
"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 **
{
"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 **
{
"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
{
"data": {
"id": "<NOTIFICATION_ID>",
"submission_id": 1620095270807 // submission ID is a timestamp of submission
},
"type": "notification:submitted"
}
notification:submission_canceled
Example
{
"data": {
"id": "<NOTIFICATION_ID>",
"canceled_at": 1620095280807,
"submission_id": 1620095270807
},
"type": "notification:submission_canceled"
}
notification:published
Example
{
"data": {
"id": "<NOTIFICATION_ID>",
"published_at": 1620095270807
},
"type": "notification:published"
}