Skip to content

Instantly share code, notes, and snippets.

@thomas-layer
Created August 11, 2015 22:27
Show Gist options
  • Save thomas-layer/6e30dfcb84e80abccff5 to your computer and use it in GitHub Desktop.
Save thomas-layer/6e30dfcb84e80abccff5 to your computer and use it in GitHub Desktop.
Layer Webhooks API

Layer Webhooks

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 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 to announcements

###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)

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 section below.

Request Headers

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 of layer-webhook-notifier/1.0 (https://developer.layer.com/docs/api/web/webhooks).
  • 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 (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.

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 (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.

Event Payloads

The remaining section details the JSON format for request bodies of each subscribable Webhook event type.

user_registered

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"
	}
}

message_sent

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"
	}
}

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 (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"
	}
}

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 (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"
	}
}

message_deleted

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"
	}
}

conversation_created

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"
	}
}

conversation_participants_updated

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"
	}
}

conversation_metadata_updated

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"
	}
}

conversation_deleted

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment