Outbound Webhooks
Courier can use webhooks to notify your application when an event occurs in your account.
Adding a New Webhook Destination
- Visit https://app.courier.com/settings/general
- Click the "+ Outbound Webhook" button
- Enter the 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
property and related resource data nested under the data
property.
Handle the Event
Courier currently supports the following event types:
message:updated
notification:submitted
notification:submission_canceled
notification:published
audiences:updated
audiences:user:matched
audiences:user:unmatched
audiences:calculated
Your webhook should be prepared to handle any of these events based on the type
property in the received payload. Additional event types may be added in the future.
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 Signatures
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 Signature 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 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 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: string) {
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 the 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.
Event Data
message:updated
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.
For example, when an email is sent, it goes from ENQUEUED
→ SENT
→ DELIVERED
→ OPENED
.
Here's a rundown of how the payloads would look for each event:
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, Courier emits notification:submitted
, notification:submission_canceled
, and notification:published
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"
}
audiences:updated
This event is fired when your audience is created or updated.
Example:
{
"data": {
"audience_id": "software-engineers",
"audience_version": 11,
"filter": {
"path": "title",
"value": "Software Engineer",
"operator": "EQ"
}
},
"type": "audiences:updated"
}
audiences:user:matched
This event is fired when a user is matched to an audience. This usually happens when a user is created or updated. If user's profile matches any of the audience's filters, the user is matched to the audience.
Example:
{
"data": {
"audience_id": "software-engineers",
"audience_version": 11,
"reason": "EQ('title', 'Software Engineer') => true",
"user_id": "suhas"
},
"type": "audiences:user:matched"
}
audiences:user:unmatched
This event is fired when a user is unmatched to an audience. This usually happens when a user is removed or updated in such a way that it no longer matches any of the audience's filters.
Example
{
"data": {
"audience_id": "favorite-fooss-players",
"audience_version": 4,
"reason": "EQ('favorite_game', 'fosss') => true, EQ('gender', 'pigeon') => true, EQ('style', 'defend') => true",
"user_id": "suhas_with_foss"
},
"type": "audiences:user:unmatched"
}
audiences:calculated
This event is fired when Courier is done calculating audiences. This is a background process that runs every time you create or update an audience. It can take variable time depending on total number of users you have created in your Courier workspace.
Example
{
"data": {
"audience_id": "software-engineers",
"user_count": 1,
"total_users": 28,
"total_users_filtered": 27
},
"type": "audiences:calculated"
}