Skip to content

Instantly share code, notes, and snippets.

@vtrehan
Created September 28, 2015 21:50
Show Gist options
  • Save vtrehan/86bd7bd92bfb66ddbc47 to your computer and use it in GitHub Desktop.
Save vtrehan/86bd7bd92bfb66ddbc47 to your computer and use it in GitHub Desktop.
# Layer Webhooks Pre-release
Webhooks allow you to develop integrations which subscribe to events within your Layer application. When a subscribed event is triggered, Layer will send an HTTP POST payload to the endpoint designated in the webhook configuration. Webhooks provide a simple, flexible mechanism that you can use to signal an external system to take action in response to messaging activity within your app. You can use Webhooks to implement things like:
* Add context to conversations: respond to messages mentioning keywords.
* Notify another user when they are @mentioned
* Send a welcome message when a user registers (build a great onboarding experience)
* Integrate a third-party service (Uber API, Weather Underground, etc) into conversations based on context
## Event Types
When configuring a Webhook, you choose which events you would like to receive payloads for by subscribing to the specific events you intend to handle.
Each event type corresponds to a specific action that can occur within your Layer application. The current set of available event types are:
| Event | Description |
|--------|--------------|
| `message.sent` | When a Message is sent. |
| `message.delivered` | When a client acknowledges delivery of a Message. |
| `message.read` | When a client marks a Message as read. |
| `message.deleted` | When a client deletes a Message (Global deletion mode only). |
| `conversation.created` | When a new Conversation is created. |
| `conversation.participants_updated` | When a Conversation is updated participant changes. |
| `conversation.metadata_updated` | When a Conversation is updated for metadata changes. |
| `conversation.deleted` | When a Conversation is deleted (Global deletion mode only). |
###Status
Every webhook created has three possible values: `unverified`, `active` and `inactive`. A newly created webhook will have the status on `unverified`.
Please refer below to the state diagram and API documentation for additional details.
![alt text](media/webhook_status_state_diagram.png "webhook_status_state_diagram.png")
##WebHooks API
### Authentication
Authentication for the webhooks API will be based on a token generated from within the developer dashboard.
This token can be generated with an expiry period and/or revoked.
The token needs to be included in the Authorization header of each HTTP request as follows:
```
Authorization: Bearer x32pYli9DCBByPUzz3CMLU9jDTYdiAacNiJrMIkdp4lTf6sb
```
If the token is missing or invalid, the server will respond with `401 Unauthorized`.
####Create a WebHook:
| Name | Type | Description |
|---------|:------:|-------------|
| `target_url` | string | Destination URL for the WebHook (must be HTTPS) |
| `event_types` | array of strings | Types of [events](#event-types) that will cause this WebHook to be triggered |
| `secret` | string | String that’s passed with the HTTP requests as an `layer-webhook-signature` header. The value of this header is computed as the HMAC hex digest of the body, using the secret as the key. |
| `target_config` | dictionary | A free form dictionary of supplemental data specific to the WebHook |
```
POST https://api.layer.com/apps/{app_id}/webhooks
{
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"secret": "1697f925ec7b1697f925ec7b"
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
> 201 Created {
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"status": "unverified",
"created_at": "2015-03-14T13:37:27Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
#####Verify the webhook
The first request to your new webhook URL will be a verification request to confirm that Layer is communicating with the correct service. This helps in avoiding your information from leaking to unintended recipients.
The verification request will be a GET request with a `verification_challenge` parameter, which is a random string. Your service should echo back the challenge parameter as the body of its response. Once Layer receives a valid response, the endpoint is considered to be a valid webhook, and Layer will begin sending notifications of file changes. Layer will not attempt to retry the verification request. Note that the `Content-Type` for the verification response will be ignored.
```
> GET https://client.example.com/layeruser/foo?verification_challenge=de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9
Expected Response < 200 OK de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9
```
####List WebHooks:
```
GET https://api.layer.com/apps/{app_id}/webhooks
> 200 OK
[
{
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"status": "active",
"created_at": "2015-03-14T13:37:27Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
},
{
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/g6ef2b54-0991-11e5-a6c0-1697f925ec7a",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/g6ef2b54-0991-11e5-a6c0-1697f925ec7a"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"conversation.deleted"
],
"status": "inactive",
"created_at": "2015-05-14T13:37:27Z",
"target_config" : {}
}
]
```
####Get single WebHook by ID:
```
GET https://api.layer.com/apps/{app_id}/webhooks/{webhook_id}
> 200 OK {
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"status": "active",
"created_at": "2015-03-14T13:37:27Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
####Activate a WebHook:
```
POST https://api.layer.com/apps/{app_id}/webhooks/{webhook_id}/activate
> 200 OK {
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"status": "unverified",
"created_at": "2015-03-14T13:37:27Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
This is a no-op if the webhook is already active.
If the webhook is unverified or disabled this results in a webhook [verification](#verify-the-webhook) request to the target_url, and transitioned to the `active` state once the verification is completed.
####Deactivate a WebHook:
```
POST https://api.layer.com/apps/{app_id}/webhooks/{webhook_id}/deactivate
> 200 OK {
"id": "layer:///apps/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b",
"url": "https://api.layer.com/082d4684-0992-11e5-a6c0-1697f925ec7b/webhooks/f5ef2b54-0991-11e5-a6c0-1697f925ec7b"
"target_url": "https://client.example.com/layeruser/foo",
"event_types": [
"user.registered",
"conversation.created"
],
"status": "inactive",
"created_at": "2015-03-14T13:37:27Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
####Delete a WebHook:
```
DELETE https://api.layer.com/apps/{app_id}/webhooks/{webhook_id}
> 204 (No Content)
```
## Webhook Requests
Each event type has a specific payload with relevant information about the event. These payloads are detailed on a per-event basis in the [Event Payloads](#event-payloads) section below.
### Request Headers
All Webhook requests are delivered with a set of HTTP headers detailing context about the event.
| Header | Description |
|--------|--------------|
| `layer-webhook-event-type` | Name of the event that triggered delivery. |
| `layer-webhook-signature` | The value of this header is computed as the HMAC hex digest of the body, using the secret config option as the key. |
| `layer-webhook-request-id` | A unique ID for this Webhook request. |
| `layer-webhook-id` | The ID for the webhook. |
* WebHook requests are delivered with a `User-Agent` of `layer-webhooks/1.0`.
* The WebHook requests will have the `Content-Type` header set to `application/json`.
### Validating Payload Integrity
The `secret` specified when a Webhook is created is used to compute a [Hash-based Message Authentication Code](http://en.wikipedia.org/wiki/HMAC) (or HMAC) over the serialized request body before it is sent. The HMAC is delivered with the request via the `layer-webhook-signature` HTTP header.
When integrating a Layer Webhook with your application, it is recommended that you verify the integrity of the payload by validating the signature given in the `layer-webhook-signature` header. You can do so by feeding the secret and complete request body to a crypto library (such as OpenSSL) that is capable of computing an HMAC digest. If the request is valid and the body has not been tampered with, the computed signature will match the header value exactly.
### Responding to a Webhook
The endpoint receiving the Layer webhook is expected to respond with a 2xx status code in order to acknowledge delivery of the request. Any non-2xx response will be considered a delivery failure and will be retried with exponential backoff for a period of time (upto 10 retries).
The retries will have the same value for the `layer-webhook-request-id` header, the target server should be able to handle duplicate transmissions of the webhook requests.
If the webhook request fails to get a response it will be transitioned to the `inactive` status and will need to be reactivated using the activation API. The Web UI will display the reason when a webhook is deactivated.
## Event Payloads
The remaining section details the JSON format for request bodies of each subscribable Webhook event type.
### `message.sent`
Triggered when a new Message is sent.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"message.sent",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f"
},
"parts": [
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/0",
"mime_type": "text/plain",
"body": "This is the message.",
"size": 20
},
{
"mime_type": "image/png",
"content": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/1",
"download_url": "http://google-testbucket.storage.googleapis.com/some/download/path",
"expiration": "2014-09-09T04:44:47+00:00",
"refresh_url": "https://api.layer.com/content/7a0aefb8-3c97-11e4-baad-164230d1df67",
"size": 172114124
},
"size": 172114124
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"id": "12345"
},
"recipient_status": {
"12345": "read",
"999": "sent",
"111": "sent"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `message.delivered`
Triggered when a Message recipient acknowledges delivery of a Message. Note: These hooks will only be triggered on small conversations of 5 or less participants.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"message.delivered",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f"
},
"parts": [
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/0",
"mime_type": "text/plain",
"body": "This is the message.",
"size": 20
},
{
"mime_type": "image/png",
"content": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/1",
"download_url": "http://google-testbucket.storage.googleapis.com/some/download/path",
"expiration": "2014-09-09T04:44:47+00:00",
"refresh_url": "https://api.layer.com/content/7a0aefb8-3c97-11e4-baad-164230d1df67",
"size": 172114124
},
"size": 172114124
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"name": "t-bone"
},
"recipient_status": {
"777": "sent",
"12345": "read",
"111": "delivered"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `message.read`
Triggered when a Message recipient marks a Message as read. Note: These hooks will only be triggered on small conversations of 5 or less participants.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"message.read",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f"
},
"parts": [
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/0",
"mime_type": "text/plain",
"body": "This is the message.",
"size": 20
},
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/1",
"mime_type": "image/png",
"content": {
"id": "layer:///content/3d0736d9-1a50-4e9a-a9b3-2400caa9e161",
"download_url": "http://google-testbucket.storage.googleapis.com/some/download/path",
"expiration": "2014-09-09T04:44:47+00:00",
"size": 172114124
},
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"id": "12345"
},
"recipient_status": {
"12345": "read",
"999": "read",
"111": "delivered"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `message.deleted`
Triggered when a Message is globally deleted.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"message.deleted",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f"
},
"parts": [
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/0",
"mime_type": "text/plain",
"body": "This is the message."
},
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/1",
"mime_type": "image/png",
"content": {
"id": "layer:///content/3d0736d9-1a50-4e9a-a9b3-2400caa9e161",
"download_url": "http://google-testbucket.storage.googleapis.com/some/download/path",
"expiration": "2014-09-09T04:44:47+00:00",
"size": 172114124
}
},
{
"mime_type": "image/jpeg",
"body": "iVBORw0KGgoAAAANSUhEUgAAACA=",
"encoding": "base64",
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67/parts/2",
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"id": "12345"
},
"recipient_status": {
"12345": "read",
"999": "read",
"111": "delivered"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `conversation.created`
Triggered when a new Conversation is created.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"conversation.created",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"conversation": {
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"created_at": "2014-09-15T04:44:47+00:00",
"messages_url": "https://api.layer.com/conversations/c12fd916-1390-464b-850f-1380a051f7c8/messages",
"distinct": false,
"participants": [
"1234",
"5678"
],
"metadata": {
"favorite": "true",
"background_color": "#3c3c3c"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `conversation.participants_updated`
Triggered when a Conversation is updated through mutation of the participants list.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"conversation.participants_updated",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"conversation": {
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"created_at": "2014-09-15T04:44:47+00:00",
"messages_url": "https://api.layer.com/conversations/c12fd916-1390-464b-850f-1380a051f7c8/messages",
"distinct": false,
"participants": [
"1234",
"5678"
],
"metadata": {
"favorite": "true",
"background_color": "#3c3c3c"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `conversation.metadata_updated`
Triggered when a Conversation is updated through mutation of metadata.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"conversation.metadata_updated",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"conversation": {
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"created_at": "2014-09-15T04:44:47+00:00",
"messages_url": "https://api.layer.com/conversations/c12fd916-1390-464b-850f-1380a051f7c8/messages",
"distinct": false,
"participants": [
"1234",
"5678"
],
"metadata": {
"favorite": "true",
"background_color": "#3c3c3c"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
### `conversation.deleted`
Triggered when a Conversation is globally deleted.
```json
{
"event_timestamp":"2015-09-17T20:46:47.561Z",
"event_type":"conversation.deleted",
"event_id":"c12f340d-3b62-4cf1-9b93-ef4d754cfe69",
"conversation": {
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"created_at": "2014-09-15T04:44:47+00:00",
"messages_url": "https://api.layer.com/conversations/c12fd916-1390-464b-850f-1380a051f7c8/messages",
"distinct": false,
"participants": [
"1234",
"5678"
],
"metadata": {
"favorite": "true",
"background_color": "#3c3c3c"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment