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
When configuring a Webhook, you can choose which payloads you would like to receive payloads for. While you can opt-in to receiving a notification for all events via the Wildcard event type, it is recommended that you only subscribe to the specific events that you intend to handle in order to limit the amount of HTTP requests generated. You can change the list of subscribed event types at any time through the Webhook Backend API or via the Webhook UI in the Layer developer dashboard.
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 |
---|---|
user_registered |
When a new user registers by authenticating for the first time. |
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). |
- Note that the
message
events also apply toannouncements
###Status
Every webhook created has three possible values: unverified
, active
and inactive
. A newly created webhook will have the status on unverified
.
##WebHooks API
####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 that will cause this WebHook to be triggered |
secret |
string | String that’s passed with the HTTP requests as an x-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 TODO: size limitations? |
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 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)
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 section below.
All Webhook requests are delivered with a set of HTTP headers detailing context about the event.
Header | Description |
---|---|
x-layer-webhook-event-type |
Name of the event that triggered delivery. |
x-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. |
x-layer-webhook-request-id |
A unique ID for this Webhook request. |
- WebHook requests are delivered with a
User-Agent
oflayer-webhook-notifier/1.0 (https://developer.layer.com/docs/api/web/webhooks)
. - The WebHook requests will have the
Content-Type
header set toapplication/json
.
The secret
specified when a Webhook is created is used to compute a Hash-based Message Authentication Code (or HMAC) over the serialized request body before it is sent. The HMAC is delivered with the request via the x-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 x-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.
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 (TODO: figure out how long).
The retries will have the same value for the x-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.
The remaining section details the JSON format for request bodies of each subscribable Webhook event type.
Triggered when a user authenticates with Layer for the first time.
{
"user_id": "12345",
"registered_at": "2015-03-08T20:20:04Z",
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
Triggered when a new Message is sent.
{
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/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",
"received_at": "2014-09-16T19:54:39+00:00",
"sender": {
"id": "12345",
"name": "t-bone"
},
"recipient_status": {
"12345": "read",
"999": "sent",
"111": "sent"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
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 (TODO: is that enough/too much?)
{
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/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",
"received_at": "2014-09-16T19:54:39+00:00",
"sender": {
"id": "12345",
"name": "t-bone"
},
"recipient_status": {
"777": "sent",
"12345": "read",
"111": "delivered"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
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 (TODO: is that enough/too much?)
{
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/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",
"received_at": "2014-09-16T19:54:39+00:00",
"sender": {
"id": "12345",
"name": "t-bone"
},
"recipient_status": {
"12345": "read",
"999": "read",
"111": "delivered"
}
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
Triggered when a Message is globally deleted.
{
"message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/082d4684-0992-11e5-a6c0-1697f925ec7b/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",
"received_at": "2014-09-16T19:54:39+00:00",
"sender": {
"id": "12345",
},
},
"target_config" : {
"key1" : "value1",
"key2" : "value2"
}
}
Triggered when a new Conversation is created.
{
"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"
}
}
Triggered when a Conversation is updated through mutation of the participants list.
{
"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"
}
}
Triggered when a Conversation is updated through mutation of metadata.
{
"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"
}
}
Triggered when a Conversation is globally deleted.
{
"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"
}
}
###Frontend
Current design uses the Platform API (backspace) as the front end for the proposed WebHook service. This lets us leverage the existing App level authorization mechanism and simply add a new endpoint for https://api.layer.com/apps/{app_id}/webhooks
Additional logic will be required for the verification of URLs.