We can forward you events in real-time to a web page of your choosing so that you integrate your own systems, for processing or analytics. You can choose what events you are interested in, so you only receive the information you want and need.
Recommendations
As you design your webhook events receiving solution these are a few recommendations to consider:
- We recommend using batches in webhooks registrations as they far more efficient than processing singular events. We simply send you an array of events, so simple to implement. See this section for more details.
- For production webhook endpoints we recommend HTTPS use to protect your data
- We use a common envelope schema for sending webhook events, so a single webhook registration can process varied event types and determine how to process the payload based on the name of the event. By having a single webhook endpoint support multiple event types you will gain more efficiencies from the batching process. See this section for more details.
- To facilitate easy processing and correlation of received events back to their sends we recommend using the metadata facility in our APIs when sending to add any useful data for processing the webhook events; this metadata will be included in the webhook events associated with the sent message
Check out our quick start for webhooks
For a guided example of creating a webhook reception page check out our quick start for receiving data via webhooks
Creating a web page to receive data
Receiving events
In order to setup a webhook registration you will need to create a web page that the event data will be posted to. The web page will have JSON data sent to it via HTTPS POST and will be expected to return an appropriate HTTP status code such as: HTTP 200 - OK response if it successfully accepts the posted data; the expected HTTP status codes are:
HTTP Status Code | Type | Description |
---|---|---|
200, 201, 2XX | OK | Data accepted |
401 | Unauthorized | Issue authenticating the sender, or HMAC not valid |
400 | Bad Request | Could not process the data sent; this will not be retried |
Any other | - | Failed to accept data, this will be retried |
Retry Policy
If a failure occurs accepting data or your server cannot be contacted we will use a gradual back off retry schema for up to 24 hours and then the event will be dropped. This system will ensure glitches and short amounts of downtime do not result in data loss, but just delays.
The retry schedule is as follows:
- 5 seconds
- 5 seconds
- 30 seconds
- 30 seconds
- 1 minute
- 2 minutes
- 5 minutes
- 10 minutes
- 15 minutes
- 30 minutes
- 1 hour
- 2 hours
- 4 hours
- 4 hours
- 4 hours
- 4 hours
- 4 hours
Processing the events
Maximum permitted time
Your system will be permitted 10 seconds to respond to a forwarded event before we will consider the server unresponsive and the event send in error. We strongly recommend that your webhook reception page validates and then queues webhook events for processing, as the mechanism is designed for data exchange and 10 seconds is not enough time to reliably process events in.
Batch vs. single events
We can forward events either individually or as a batch of events. We strongly recommend that you design your webhook reception page to accept batches of events, as this can greater improves efficiency, especially during peak times.
Batches
Batches of events are very simply a JSON array of individual webhook events.
You can control the maximum amount of events in a batch as part of your webhooks configuration [1-500] and the maximum amount of time we should wait before sending the batch of events [1-60 seconds].
We send the batch of events when either of the conditions below is met:
- The batch is full; it hit the maximum number of events permitted; or...
- The time since the last batch was sent has exceeded the batch timeout limit
By tweaking the values for the batch timeout and maximum batch size you can finely tune the webhook between efficiency and responsiveness.
Single events
Each forwarded event is sent individually, and therefore at peak times your system must be able to accept many parallel calls.
{
"eventId": "ca58832d-d67a-412e-9b28-e51b675ea142",
"accountId": 123,
"apiSpaceId": "c124df6e-4352-4b26-a32a-c3032bea7a01",
"name": "message.sent",
"payload": {
"id": "ec7e182f-4d87-4135-b989-b26ab8d74f05",
"details": {
"channel": "sms",
"additionalInfo": {
"to": "441231123123",
"successful": true
},
"channelStatus": {
"sms": {
"status": "sent",
"details": {
"to": "441231123123",
"successful": true
},
"updatedOn": "2017-04-11T08:19:48.106Z"
}
}
},
"updatedOn": "2017-04-11T08:19:48.106Z"
},
"revision": 2,
"etag": "\"2e-PNWSn3HlxaIB/CYz7LaR1XhMvDE\"",
"timestamp": "2017-04-11T08:19:48.494Z"
}
[
{
"eventId": "ca58832d-d67a-412e-9b28-e51b675ea142",
"accountId": 123,
"apiSpaceId": "c124df6e-4352-4b26-a32a-c3032bea7a01",
"name": "message.sent",
"payload": {
"id": "ec7e182f-4d87-4135-b989-b26ab8d74f05",
"details": {
"channel": "sms",
"additionalInfo": {
"to": "441231123123",
"successful": true
},
"channelStatus": {
"sms": {
"status": "sent",
"details": {
"to": "441231123123",
"successful": true
},
"updatedOn": "2021-04-11T08:19:48.106Z"
}
}
},
"updatedOn": "2021-04-11T08:19:48.106Z"
},
"revision": 2,
"etag": "\"2e-PNWSn3HlxaIB/CYz7LaR1XhMvDE\"",
"timestamp": "2021-04-11T08:19:48.494Z"
},
{
"eventId": "1b4d4b5e-4176-4c91-a01c-3c927bd75a1a",
"accountId": 123,
"apiSpaceId": "c124df6e-4352-4b26-a32a-c3032bea7a01",
"name": "message.sent",
"payload": {
"id": "fc1df0c5-35b8-4d34-bcc3-1994df7b6cdf",
"details": {
"channel": "sms",
"additionalInfo": {
"to": "44321321321",
"successful": true
},
"channelStatus": {
"sms": {
"status": "sent",
"details": {
"to": "44321321321",
"successful": true
},
"updatedOn": "2021-04-11T08:19:48.106Z"
}
}
},
"updatedOn": "2021-04-11T08:19:48.106Z"
},
"revision": 2,
"etag": "\"2e-PNWSn3HlxaIB/CYz7LaR1XhMvDE\"",
"timestamp": "2021-04-11T08:19:48.494Z"
}
]
Event message structure
Each event will be sent in a standard envelope message with a payload that is the actual event data. The event payloads are documented in the sub sections following this page, such as App Messaging - Message Events .
The envelope format is:
Property | Type | Description |
---|---|---|
eventId | string | The unique identifier for this event |
accountId | int | The account id the event is associated with |
apiSpaceId | string | The API Space Id the event is from |
name | string | The name/type of the event being received |
payload | object | The specific event data (see this sections pages for more details) |
revision | int | The revision number for the event which increments with each event |
etag | string | The ETag entity hash, commonly used for detecting change |
timestamp | date time | The UTC time the event occurred in ISO 8601 format |
The following is an example event you could receive:
{
"eventId": "ca58832d-d67a-412e-9b28-e51b675ea142",
"accountId": 123,
"apiSpaceId": "c124df6e-4352-4b26-a32a-c3032bea7a01",
"name": "message.sent",
"payload": {
"id": "ec7e182f-4d87-4135-b989-b26ab8d74f05",
"details": {
"channel": "sms",
"additionalInfo": {
"to": "441231123123",
"successful": true
},
"channelStatus": {
"sms": {
"status": "sent",
"details": {
"to": "441231123123",
"successful": true
},
"updatedOn": "2017-04-11T08:19:48.106Z"
}
}
},
"updatedOn": "2017-04-11T08:19:48.106Z"
},
"revision": 2,
"etag": "\"2e-PNWSn3HlxaIB/CYz7LaR1XhMvDE\"",
"timestamp": "2017-04-11T08:19:48.494Z"
}
{
"eventId": "e8b015b0-dd11-4f2d-a4b8-52c20739e16b",
"accountId": 123,
"apiSpaceId": "c124cf6e-4352-4b26-a71a-c3032bea7a01",
"name": "profile.create",
"payload": {
"id": "Bob Smith"
},
"revision": 0,
"etag": "\"15-w27xhc3Oc4ZW/h3hpKppJQ88/rU\"",
"timestamp": "2017-04-10T14:52:29.484Z"
}
Testing made simple
You can test receiving events simply by creating a request bin page for free, setting this as the URL for your webhook. Alternatively if you want to receive the data to your development machine then NGROK is a great tool for achieving this.
Security
The following processing and guidance should be followed in order to ensure your webhook remains secure:
HTTPS Recommended
We will forward data to either HTTP or HTTPS URLs, but we recommend using HTTPS connections to ensure data privacy. Please ensure your web page can accept HTTPS requests, and uses a certificate from a public certificate authority as we cannot accept self signed certificates.
Authenticating and verifying data
We use HMAC hashes to both authenticate and ensure the data has not been altered in transit.
We will use the secret you configure on your webhook settings to create a hash of the event data (HTTP body) using the HMAC SHA-1 algorithm and then store this in the requests HTTP headers as X-Comapi-Signature. When you receive data via your webhook page your must create a hash of the received request body using UTF-8 encoding with the SHA-1 algorithm and your secret, and then compare it against the received hash from the X-Comapi-Signature HTTP header. If they match you can trust the data, otherwise you should reject the data and return a HTTP 401.
Ensure you calculate the HMAC on the raw body
Many web frameworks will give you access to a web requests body data but after they have interpreted it e.g. created a object from JSON. Any change to the data at all will cause the HMAC hash to differ, and therefore it is very important to calculate your HMAC on the raw HTTP body data. See the Webhook Quick Starts for coding examples of how to do this.
The HMAC is not Base64 encoded!
Some out of the box HMAC SHA-1 algorithms return the HMAC result Base64 encoded, please note we do not Base64 encode the HMAC result!
Revision processing
The Revision property will increment for each message type, therefore giving you an indication of relative order. The sequence may not be contiguous, but will always increment as more events are generated.
In some circumstances the Revision property value can be used to recognise when it is safe to discard data, such as when you receive a message status update with a lower revision id than the last one you processed for a message.
We do not guarantee events in the correct sequence
Due to the nature of large distributed systems we cannot guarantee the order events will be forwarded in exactly the chronological order they occurred in, and therefore the provide a Revision property to help you sequence the events when you receive them.
Deduping
On rare occasions we will send an event more than once, this is called the "at least once" pattern. To avoid issues we recommend using the eventid to dedupe received events.
Setting up to receive data from the webhook
Configure the forwarding
To setup or modify a webhook registration you can use the webhooks service.
Webhook events
Your webhook will need to subscribe to only those events it can make use of. The events and categories are described in the subsections below, or by calling the webhook services available events method. (See below for example JSON returned)
Some webhook events support filters to narrow down which events you are interested further
e.g.
The message.delivered event has an optional filter of channel so that you can filter down to receiving just a single channels receipts if required. The JSON returned from the webhook services availableevents method for this event is as follows:
{
"type": "message.delivered",
"description": "Details of any outbound message \"delivered\" status updates.",
"filters": [
{
"name": "channel",
"required": false,
"description": "Channel that the event relates to"
}
]
}
Only take the events you need!
To maximise webhook performance and to save your system having to process events that you don't need which take up resources please only select the webhooks events you need.
Making the service calls
HTTP tools such as Postman or curl are recommended to make the calls to the webhook service.
Adding a new webhook registration
To add a new webhook registration do the following:
- Select what events you would like to subscribe to from the events and categories described in the subsections, or by calling the webhook services availableevents method. (See below for example JSON returned, and example webhook requests for popular use cases)
- Call the POST method on the webhooks service with your webhooks URI, event selection, secret and batch settings.
Use a strong secret
Please ensure your secret string is at least 16 characters long, but we recommend 36 characters or more.
Example Create Webhook Requests
{
"name": "test-webhook",
"url": "https://webhooks.acme.com",
"secret": "a strong secret",
"batch": true,
"maxBatchSize": 50,
"batchTimeout": 30,
"subscriptions": [
{
"type": "message.sent",
"filters": []
},
{
"type": "message.delivered",
"filters": []
},
{
"type": "message.read",
"filters": []
},
{
"type": "message.expired",
"filters": []
},
{
"type": "message.failed",
"filters": []
},
{
"type": "message.inbound",
"filters": []
}
]
}
{
"name": "test-webhook",
"url": "https://webhooks.acme.com",
"secret": "a strong secret",
"bacth": true,
"maxBatchSize": 50,
"batchTimeout": 30,
"subscriptions": [
{
"type": "chat.create",
"filters": []
},
{
"type": "chat.channelUpdated",
"filters": []
},
{
"type": "chat.update",
"filters": []
},
{
"type": "chat.teamChanged",
"filters": []
},
{
"type": "chat.closed",
"filters": []
},
{
"type": "chat.delete",
"filters": []
},
{
"type": "chat.participantAdded",
"filters": []
},
{
"type": "chat.participantRemoved",
"filters": []
},
{
"type": "chat.status",
"filters": []
},
{
"type": "chatMessage.sent",
"filters": []
},
{
"type": "chatMessage.delivered",
"filters": []
},
{
"type": "chatMessage.read",
"filters": []
}
]
}
{
"name": "test-webhook",
"url": "https://webhooks.acme.com",
"secret": "a strong secret",
"bacth": true,
"maxBatchSize": 50,
"batchTimeout": 30,
"subscriptions": [
{
"type": "profile.create",
"filters": []
},
{
"type": "profile.update",
"filters": []
},
{
"type": "profile.delete",
"filters": []
},
{
"type": "profile.undelete",
"filters": []
}
]
}
{
"name": "test-webhook",
"url": "https://webhooks.acme.com",
"secret": "a strong secret",
"bacth": true,
"maxBatchSize": 50,
"batchTimeout": 30,
"subscriptions": [
{
"type": "conversation.create",
"filters": []
},
{
"type": "conversation.update",
"filters": []
},
{
"type": "conversation.delete",
"filters": []
},
{
"type": "conversation.undelete",
"filters": []
},
{
"type": "conversation.participantAdded",
"filters": []
},
{
"type": "conversation.participantUpdated",
"filters": []
},
{
"type": "conversation.participantDeleted",
"filters": []
},
{
"type": "conversationMessage.sent",
"filters": []
},
{
"type": "conversationMessage.delivered",
"filters": []
},
{
"type": "conversationMessage.read",
"filters": []
},
{
"type": "presence.available",
"filters": []
},
{
"type": "presence.away",
"filters": []
},
{
"type": "presence.offline",
"filters": []
}
]
}
Example output from the webhook services availableevents method:
[
{
"type": "conversation.create",
"description": "Details of any conversation that was created",
"filters": []
},
{
"type": "conversation.update",
"description": "Details of any conversation that was updated",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversation.delete",
"description": "Details of any conversation that was deleted",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversation.undelete",
"description": "Details of any conversation that was deleted, but then recreated",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversation.participantAdded",
"description": "Details of any participant added to a conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversation.participantUpdated",
"description": "Details of any participant updated for a conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversation.participantDeleted",
"description": "Details of any participant removed from a conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
}
]
},
{
"type": "conversationMessage.sent",
"description": "Details of any message sent to a conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
},
{
"name": "from",
"required": false,
"description": "id of the profile that sent the message"
},
{
"name": "scope",
"required": false,
"description": "The scope of the message (a2p / p2p)"
},
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound) - only applicable to scope=a2p messages"
}
]
},
{
"type": "conversationMessage.delivered",
"description": "Details of any message successfully delivered to a user for a given conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
},
{
"name": "scope",
"required": false,
"description": "The scope of the message (a2p / p2p)"
},
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound) - only applicable to scope=a2p messages"
}
]
},
{
"type": "conversationMessage.read",
"description": "Details of any message read by a user for a given conversation",
"filters": [
{
"name": "conversationId",
"required": false,
"description": "id of the conversation that the event relates to"
},
{
"name": "scope",
"required": false,
"description": "The scope of the message (a2p / p2p)"
},
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound) - only applicable to scope=a2p messages"
}
]
},
{
"type": "message.sent",
"description": "Details of any outbound message successfully sent.",
"filters": []
},
{
"type": "message.delivered",
"description": "Details of any outbound message \"delivered\" status updates.",
"filters": [
{
"name": "channel",
"required": false,
"description": "Channel that the event relates to"
}
]
},
{
"type": "message.read",
"description": "Details of any outbound message \"read\" status updates.",
"filters": [
{
"name": "channel",
"required": false,
"description": "Channel that the event relates to"
}
]
},
{
"type": "message.expired",
"description": "Details of any outbound message \"expired\" status updates.",
"filters": [
{
"name": "channel",
"required": false,
"description": "Channel that the event relates to"
}
]
},
{
"type": "message.failed",
"description": "Details of any outbound message that failed to send over any channel.",
"filters": []
},
{
"type": "message.inbound",
"description": "Details of any inbound message sent.",
"filters": [
{
"name": "channel",
"required": false,
"description": "Channel that the event relates to"
}
]
},
{
"type": "profile.create",
"description": "Details of any profile that was created",
"filters": []
},
{
"type": "profile.update",
"description": "Details of any profile that was updated",
"filters": []
},
{
"type": "profile.delete",
"description": "Details of any profile that was deleted",
"filters": []
},
{
"type": "profile.undelete",
"description": "Details of any profile that was deleted, but then recreated",
"filters": []
},
{
"type": "facebook.optin",
"description": "Opt-in event from Facebook.",
"filters": []
},
{
"type": "session.sessionStarted",
"description": "Details of any session that was successfully authenticated",
"filters": []
},
{
"type": "chat.create",
"description": "Chat creation event",
"filters": []
},
{
"type": "chat.channelUpdated",
"description": "Chat channel update event",
"filters": []
},
{
"type": "chat.update",
"description": "Chat update event",
"filters": []
},
{
"type": "chat.teamChanged",
"description": "Chat team change event",
"filters": []
},
{
"type": "chat.closed",
"description": "Chat close event",
"filters": []
},
{
"type": "chat.delete",
"description": "Chat delete event",
"filters": []
},
{
"type": "chat.participantAdded",
"description": "Chat participant added event",
"filters": []
},
{
"type": "chat.participantRemoved",
"description": "Chat participant removed event",
"filters": []
},
{
"type": "chat.status",
"description": "Chat status change event",
"filters": []
},
{
"type": "chatMessage.sent",
"description": "Details of any message sent to a chat",
"filters": [
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound)"
},
{
"name": "teamId",
"required": false,
"description": "The id of the team that sent the message"
}
]
},
{
"type": "chatMessage.delivered",
"description": "Details of any message successfully delivered to a user for a given chat",
"filters": [
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound)"
}
]
},
{
"type": "chatMessage.read",
"description": "Details of any message read by a user for a given chat",
"filters": [
{
"name": "direction",
"required": false,
"description": "The direction of the message (inbound / outbound)"
}
]
},
{
"type": "presence.available",
"description": "Profile available event",
"filters": []
},
{
"type": "presence.away",
"description": "Profile away event",
"filters": []
},
{
"type": "presence.offline",
"description": "Profile offline event",
"filters": []
}
]