Webhooks

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 CodeTypeDescription
200, 201, 2XXOKData accepted
401UnauthorizedIssue authenticating the sender, or HMAC not valid
400Bad RequestCould 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 move the event to dead queue. This system will ensure glitches and downtime do not result in data loss, but it will be delayed.

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.

1171

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:

PropertyTypeDescription
eventIdstringThe unique identifier for this event
accountIdintThe account id the event is associated with
apiSpaceIdstringThe API Space Id the event is from
namestringThe name/type of the event being received
payloadobjectThe specific event data (see this sections pages for more details)
revisionintThe revision number for the event which increments with each event
etagstringThe ETag entity hash, commonly used for detecting change
timestampdate timeThe 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:

  1. 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)
  2. 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": []
    }
]