This document outlines the proposed structure of the Layer Messaging REST API. It is the underlying foundation of the Layer Web SDK and enables integration with the Layer platform using standard web technologies.
Spec Changes:
- sender.id is now sender.user_id
- Distinct Conversations can now be created with Metadata, but requires special handling.
- When sending a message, the
push
property has been renamed tonotification
The following list of features indicates the following:
done
: Feature is implemented and ready to useplanned
: Feature is schedule to be done soonfuture
: Feature will be done after the first GA releaseredesign
: The feature was recently redesigned; please check if this breaks your implementationnew
: New feature added to specdeprecated
: Feature still works but will soon be removed
Features:
- General
- [Obtaining a Nonce] (#obtaining-a-nonce):
done
- [Authenticating with an Identity Token] (#authenticating-with-an-identity-token):
done
- [Ending a Session] (#ending-a-session):
future
- [Link Header] (#link-header):
done
- [Conditional Requests] (#conditional-requests):
future
- [De-duplication] (#de-duplication):
planned
- [Root Request] (#root-request):
done
- [Obtaining a Nonce] (#obtaining-a-nonce):
- [Conversations] (#conversations)
- [Listing Conversations] (#listing-conversations):
done
- [Pagination] (#pagination):
planned
- [Pagination] (#pagination):
- [Retrieving a Conversation] (#retrieving-a-conversation):
done
- [Creating a Conversation] (#creating-a-conversation):
done
- [Distinct Conversations] (#distinct-conversations):
done
redesigned
- [Distinct Conversations] (#distinct-conversations):
- [Deleting A Conversation] (#deleting-a-conversation):
done
- [Add/Remove Participants] (#addremove-participants)
done
- [Metadata] (#metadata)
- [Patching Metadata Structures] (#patching-metadata-structures):
done
- [Patching Metadata Structures] (#patching-metadata-structures):
- [Writing Receipts] (#writing-receipts-for-all-messages-in-a-conversation):
future
- [Listing Conversations] (#listing-conversations):
- [Messages] (#messages)
- [Listing Messages in a Conversation] (#listing-messages-in-a-conversation):
done
- [Pagination] (#pagination):
planned
- [Pagination] (#pagination):
- [Retrieving a Message] (#retrieving-a-message):
done
- [Sending a Message] (#sending-a-message):
done
- [Sending a Message with Metadata] (#sending-a-message-with-metadata):
future
- [Push Notifications] (#push-notifications):
done
,redesign
- [Sending a Message with Metadata] (#sending-a-message-with-metadata):
- [Deleting a Message] (#deleting-a-message):
done
- [Delivery & Read Receipts] (#delivery--read-receipts):
- [Writing a Receipt for a Message] (#writing-a-receipt-for-a-message):
done
- [Writing Receipts for Specific Messages] (#writing-receipts-for-specific-messages):
future
- [Writing a Receipt for a Message] (#writing-a-receipt-for-a-message):
- [Metadata] (#metadata):
future
- [Rich Content] (#rich-content)
- [Initiating a Rich Content Upload] (#initiating-a-rich-content-upload):
done
- Sending a Message Including Rich Content:
done
- [Downloading a Rich Content Message Part] (#downloading-a-rich-content-message-part):
done
- [Refreshing the Download URL for a Content Object] (#refreshing-the-download-url-for-a-content-object):
done
- [Initiating a Rich Content Upload] (#initiating-a-rich-content-upload):
- [Listing Messages in a Conversation] (#listing-messages-in-a-conversation):
- [Querying] (#querying)
planned
The Layer REST API is designed to be a consistent, simple, and leverage existing standards as much as possible. This overview sets forth the general design principles and common behaviors of the API. Specifics about each resource are covered in detail later.
The API is versioned using a custom media type that encodes the wire format and the version desired. Developers must explicitly request a specific version via the Accept
header:
Accept: application/vnd.layer+json; version=1.0
Failure to request a specific version of the API will result in
> 406 (Not Acceptable)
{
id: "invalid_header",
code: 107,
message: "Invalid Accept header; must be of form application/vnd.layer+json; version=x.y",
url: "https://github.com/layerhq/docs/blob/web-api/specs/rest-api.md#api-versioning",
data: {
header: "Accept"
}
}
All API access is over HTTPS, and accessed from the api.layer.com
domain. All data is sent and received as JSON. The Content-Type
for all responses is application/json; charset=utf-8
. The version of the API that generated the response is included via the
X-Layer-API-Version
header.
$ curl -i https://api.layer.com/conversations -H "Accept:application/vnd.layer+json; version=1.0"
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 9 Oct 2014 13:23:14 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Status: 200 OK
ETag: "a00049ba79152d03380c34652f2cb612"
X-Layer-API-Version: 1.0
Content-Length: 5
Cache-Control: max-age=0, private, must-revalidate
X-Content-Type-Options: nosniff
[]
Blank fields are included as null
instead of being omitted.
All timestamps are returned in ISO 8601 format:
YYYY-MM-DDTHH:MM:SSZ
When retrieving a list of resources, the response may include a subset of the overall attributes for that resource. Such a representation is the "summary" of the current state of the resource. You can obtain a full representation of the resource by fetching the detail resource.
When you retrieve an individual resource, the response will include all attributes for that resource.
The resource specific documentation will specify exactly which attributes are returned for a given request.
Many API methods take optional parameters. For GET requests, any parameters not specified as a segment in the path can be passed as an HTTP query string parameter:
$ curl -i "https://api.layer.com/conversations?count=5"
In this example, an optional parameter of count
is passed in the query string.
For POST
, PATCH
, PUT
, and DELETE
requests parameters not included in the URL must be passed as JSON with a Content-Type of ‘application/json’:
$ curl -i -H "Accept:application/vnd.layer+json; version=1.0" -d '{"identity_token":"Some token here", "app_id": "your app id"}' https://api.layer.com/sessions
Clients must be prepared to handle a variety of errors that may be returned by the server. Errors responses will have an appropriate HTTP status code and the response body will contain a JSON representation of the error.
HTTP/1.1 422 Unprocessable Entity
Content-Length: 149
{
"id": "missing_property",
"code": 104,
"message": "The participants list cannot be omitted.",
"url": "https://developer.layer.com/api.md#creating-a-conversation",
"data": {
"property": "participants"
}
}
Name | Type | Description | Example |
---|---|---|---|
id |
string | Unique string error identifier. | access_denied |
code |
integer | Unique numeric error code. | 12345 |
message |
string | Details of the error. | The participants list cannot be omitted |
url |
string | A URL to a reference with more info about the error. | https://developer.layer.com/api.md#access-denied |
data |
dictionary | A free form dictionary of supplemental data specific to the error. | { "nonce": "38ca1bb2-2560-44d4-88bb-5989ce9b2b66" } |
code |
id |
Context | HTTP Status | Description |
---|---|---|---|---|
1 | service_unavailable |
Client | 503 (Service Unavailable) |
The operation could not be completed because a backend service could not be accessed. |
2 | invalid_app_id |
Client | 403 (Forbidden) |
The client provided an invalid Layer App ID. |
3 | invalid_request_id |
Client | 400 (Bad Request) |
The client has supplied a request ID that is not a valid UUID. |
4 | authentication_required |
Client | 401 (Unauthorized) |
The action could not be completed because the client is unauthenticated. The response will include a nonce for satisfying an authentication challenge. |
5 | app_suspended |
Client | 403 (Forbidden) |
The app has been suspended. |
6 | user_suspended |
Client | 403 (Forbidden) |
The authenticated user has been suspended. |
7 | rate_limit_exceeded |
Client | 429 (Too Many Requests) |
The client has sent too many requests in a given amount of time. |
8 | request_timeout |
Client | 408 (Request Timeout) or None |
The server or the client timed out waiting for a request to complete. |
9 | invalid_operation |
Client | 422 (Unprocessable Entity) or None |
The server or client has declined to perform an invalid operation (i.e. deleting an unsent message). |
10 | invalid_request |
Client | 400 (Bad Request) |
The request is structurally invalid. |
101 | access_denied |
Resource | 403 (Forbidden) |
The authenticated user does not have access to the resource requested. |
102 | not_found |
Resource | 404 (Not Found) |
The resource requested could not be found. |
103 | object_deleted |
Resource | 410 (Gone) |
The client requested a resource that has been deleted. |
104 | missing_property |
Resource | 422 (Unprocessable Entity) |
A property with a required value was not supplied. |
105 | invalid_property |
Resource | 422 (Unprocessable Entity) |
A property was supplied with an invalid value. |
106 | invalid_endpoint |
Client | 404 (Not Found) |
The endpoint 'GET /nonce' does not exist |
107 | invalid_header |
Client | 406 (Not Acceptable) |
Invalid Accept header; must be of form application/vnd.layer+json; version=x.y |
108 | conflict |
Resource | 409 (Conflict) |
The distinct conversation already exists with conflicting metadata. |
Authentication is done using the same mechanism that is used to authenticate the Layer native mobile platform SDKs (see our Authentication Guide for reference). The authentication process is begun by requesting a nonce value from the REST API. Once a nonce is obtained, the authenticating client has 10 minutes to present it to an identity provider and obtain an external identity token. This token can then be used to authenticate with the API and obtain a session token. The session token may then be used for all subsequent API calls by providing it as an HTTP header.
The details of this authentication flow is further illuminated in the individual resource documentation.
This feature is not available as of 6/22/2015.
When a client issues a network request, it is always possible for a network request to timeout or return a recoverable error status code indicating that the request should be resent. However, this opens up the possibility of the same request being performed more than once, which could have undesirable effects, especially for requests that create new entities (e.g. conversations or messages). To avoid this, it is recommended that clients include an If-None-Match header field with an RFC 4122-compliant UUID as the entity tag for requests that create new entities. If this header field exists in a request, the server will not re-execute the request if the tag it has already been seen, and will respond with the previously created resource.
If-None-Match: "7a0aefb8-3c97-11e4-baad-164230d1df67"
This header field can be omitted for applications than can tolerate such requests being occasionally re-executed.
All resources may have one or more *_url
properties linking to other resources. These are meant to provide explicit URLs such that API clients don't need to construct URLs on their own. It is recommended that API clients utilize these properties as it makes API updates easier. All URLs are returned as absolute URL values.
This feature is not available as of 6/22/2015.
Requests that return multiple items may return them in pages. Your initial request can specify a page size and whether you want to receive the first or the last page. The Link header in the response will contain URLs for first
, last
, next
and prev
pages, which can be used to obtain further pages, ordered according to the specification of the request. It is important to follow these Link header values instead of constructing your own URLs. Note that the server may choose to return a page size smaller than requested.
The underlying resource may change between requests for successive pages. The next
link will return the items currently following the last item in the page with the Link header, and the prev
link will return the items currently preceding the first item in the page with the Link header. So while the next
or prev
pages could have changed since the original request was made, there will not be gaps in the pages.
For example, the following queries can be used to retreive the first and last pages of messages in a conversation, with a page size of 50.
GET https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page=first
GET https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page=last
The Link header will contain links for the next and previous pages as follows:
link: <https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page_after=940de862-3c96-11e4-baad-164230d1df67>; rel=next,
<https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page_before=7a0aefb8-3c97-11e4-baad-164230d1df67>; rel=prev,
<https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page=first>; rel=first,
<https://api.layer.com/conversations/e69e07fd-dab3-4e12-8305-f4f61fe5ec04/messages?page_size=50&page=last>; rel=last
The possible rel
values are:
Name | Description |
---|---|
next | The URL of the next page of results. |
prev | The URL of the previous page of results. |
first | The URL of the first page of results. |
last | The URL of the last page of results. |
WITH tt AS (SELECT id, datavalue, row_number() OVER (ORDER BY sortfield) AS r FROM t ORDER BY sortfield) SELECT id, datavalue FROM tt where r > (SELECT r FROM tt WHERE id = x) LIMIT p;
(The second `ORDER BY sortfield` may not be strictly necessary.)
What this does is to use a CTE to create `tt`, which is ordered as desired and has a row number, then selects from `tt` using this row number, starting with the row number corresponding to the specified id, and limits the results to p.
This feature is not available as of 6/22/2015.
Most responses return an ETag
header. Many responses also return a Last-Modified
header. You can use the values of these headers to make subsequent requests to those resources using the If-None-Match
and If-Modified-Since
headers, respectively. If the resource has not changed, the server will return a 304 Not Modified
response.
Some resources within the Layer API expose mutability for certain properties. When mutability is supported, the resource will accept requests using the HTTP PATCH
method as detailed in the Layer-Patch Specification. Patch requests have different semantics from the more familiar HTTP methods for creating and updating resources (POST
and PUT
, respectively). When utilizing the PATCH
method, the request body must include a set of changes that are to be applied to the resource (as opposed to a representation of the resource itself).
When deleting content within the Layer API you have the option of deleting or destroying the content.
- delete: The content is deleted from all of the clients associated with the authenticated user.
- destroy: The content is deleted from all of the clients of all users with access to it.
Selection between delete and destroy is done using the destroy=boolean
parameter.
DELETE /conversations/{conversation_id}?destroy=true
DELETE /messages/{message_id}?destroy=true
This feature is not available as of 6/22/2015.
DELETE /conversations/{conversation_id}?destroy=false
DELETE /messages/{message_id}?destroy=false
This section details the individual resources that make up the API.
> 406 (Not Acceptable)
Returned when content negotiation failed (i.e. XML was requested).
> 422 (Unprocessable Entity)
Returned when the submitted document could not be processed (POST, PATCH, PUT, or DELETE)
> 429 (Too Many Requests)
Returned when the client has been rate limited. Includes an error representation detailing the limits.
> 503 (Service Unavailable)
Returned when the service is unavailable for whatever reason.
To find the endpoints that are available, you can send the following request:
GET /
>> 204 (No Content)
The endpoint info is in the Link header.
link: <https://api.layer.com/nonces>; rel=nonces,
<https://api.layer.com/sessions>; rel=sessions,
<https://api.layer.com/conversations>; rel=conversations,
<https://api.layer.com/content>; rel=content
The possible rel
values are:
Name | Description |
---|---|
nonces | The URL to load a nonce. |
sessions | The URL to post an identity token to get a session token. |
conversations | The URL to load the first page of conversations. |
content | The URL for creating external content. |
POST /nonces
>> 201 (Created)
{ "nonce": "b7a5fba5ad402d072013c1949481c1080860ff32" }
>> 429 (Too Many Requests)
{
"id": "rate_limit_exceeded ",
"code": 7,
"message": "Rate limit exceeded",
"url": "https://developer.layer.com/api.md#rate-limiting"
}
An External Identity Token and Layer App ID must be presented to authenticate.
POST /sessions
{ "identity_token": "f6179ecb285c669c07415011f17d7a4e59ce1f91.9afd0f5ef6df7bf7eb13e9ada65fa28cf765a51c.450b81833898cb159f3cfc5a9a839187e63683e0",
"app_id": "e49e50aa-ffda-453f-adc8-404f68de84ae" }
>> 201 (Created)
{ "session_token": "c3ba507fc4fc3c8e0618c4bee3250132e86bd7e9" }
>> 422 (Unprocessable Entity)
{
"id": "invalid_property",
"code": 105,
"message": "Invalid identity token; go to the developer dashboard's authentication tab and use the identity token validation form for more details.",
"url": "https://developer.layer.com/",
"data": {
"property": "identity_token"
}
}
The identity token returned from a successful request must be presented with each authenticated request on the Authorization
header with a value Layer session-token="c3ba507fc4fc3c8e0618c4bee3250132e86bd7e9"
.
The /sessions endpoint includes info in the Link header. It is important to follow these Link header values instead of constructing your own URLs.
link: <https://api.layer.com/conversations>; rel=conversations,
<https://api.layer.com/content>; rel=content,
<https://api.layer.com/websocket>; rel=websocket
The possible rel
values are:
Name | Description |
---|---|
conversations | The URL to load the first page of conversations. |
content | The URL for creating external content. |
websocket | The URL for establishing a websocket |
When the session expires, requests using the expired session token will be rejected by the API. Such requests will be challenged with a 401 (Unauthorized) response, and will include a new nonce in the response body. This nonce can then be used to obtain a new external identity token from the authentication provider, which can in turn be used to obtain a new session token.
GET https://api.layer.com/conversations
>> 401 (Unauthorized)
{
"id": "authentication_required",
"code": 4,
"message": "The session token is no longer valid because it has expired.",
"url": "https://developer.layer.com/api.md#authentication",
"data": {
"nonce": "38a9b5a41725ec2bbb51ce43328b671731496f1f"
}
}
POST http://example.com/authenticate
{ "username": "user@example.com", "password": "whatever", "layer_nonce": "38a9b5a41725ec2bbb51ce43328b671731496f1f" }
>> 201 (Created)
{ "layer_identity_token": "b1dbdad67f4579f6a1d4acf5d94d9ebb2a7fcbd7.78f5ec69d4c4b496f1d28a1c9773795f06bd0d53.567bc8b01627ae61a3ec78cd882f20fef4c15f64" }
POST https://api.layer.com/sessions
{ "identity_token": "b1dbdad67f4579f6a1d4acf5d94d9ebb2a7fcbd7.78f5ec69d4c4b496f1d28a1c9773795f06bd0d53.567bc8b01627ae61a3ec78cd882f20fef4c15f64", "app_id": "e49e50aa-ffda-453f-adc8-404f68de84ae" }
>> 201 (Created)
{ "session_token": "e679cf5b570b9e992767f0afee06a956730eea33" }
GET https://api.layer.com/conversations
>> 200 (OK)
[]
This feature is not available as of 6/22/2015.
To delete a session from the server (logout):
DELETE https://api.layer.com/sessions/{session-token}
>> 204 (No Content)
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "The resource requested could not be found.",
"url": "https://developer.layer.com/api.md#authentication"
}
>> 403 (Unauthorized)
{
"id": "access_denied",
"code": 101,
"message": "The session token is not yours to delete.",
"url": "https://developer.layer.com/api.md#authentication",
"data": {
"nonce": "38a9b5a41725ec2bbb51ce43328b671731496f1f"
}
}
{
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"metadata_url": "https://api.layer.com/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67/metadata",
"participants_url": "https://api.layer.com/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67/participants",
"messages_url": "https://api.layer.com/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67/messages",
"created_at": "2014-09-15T04:44:47+00:00",
"last_message": {
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67",
"position": 15032697020,
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/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/7a0aefb8-3c97-11e4-baad-164230d1df67",
"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
}
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"user_id": "1234",
"name": "t-bone"
},
"is_unread": true,
"recipient_status": {
"777": "sent",
"999": "read",
"111": "delivered",
"1234": "read"
}
},
"participants": [
"1234",
"777",
"999",
"111"
],
"distinct": true,
"unread_message_count": 3,
"metadata": {
"title": "Who likes this conversation?",
"favorite": "true",
"background_color": "#3c3c3c",
"likes": "5",
"likers": {
"user1": "3",
"user8": "2"
}
}
}
GET /conversations
>> 200 (OK)
Array of Conversation Representations
Paging of Conversations is not available as of 6/22/2015.
Note that metadata
is an optional property; see [Metadata] (#metadata) for rules on metadata
syntax.
POST /conversations
{
"participants": [ "1234", "777", "999", "111" ],
"distinct": false,
"metadata": {
"title": "Who likes this conversation?",
"favorite": "true",
"background_color": "#3c3c3c",
"likes": "5",
"likers": {
"user1": "3",
"user8": "2"
}
}
}
>> 201 (Created)
Conversation Representation
>> 422 (Unprocessable Entity) [Participants Not Given]
{
"id": "missing_property",
"code": 104,
"message": "The participants array cannot be blank.",
"url": "https://developer.layer.com/api.md#conversations",
"data": {
"property": "participants"
}
}
>> 422 (Unprocessable Entity) [Invalid Participants]
{
"id": "invalid_property",
"code": 104,
"message": "The value of the participants property must be an array of user ID string values.",
"url": "https://developer.layer.com/api.md#conversations",
"data": {
"property": "participants"
}
}
>> 422 (Unprocessable Entity) [Too Many Participants]
{
"id": "invalid_property",
"code": 105,
"message": "The participants array exceeds the maximum size of 25.",
"url": "https://developer.layer.com/api.md#conversations",
"data": {
"property": "participants"
}
}
>> 422 (Unprocessable Entity) [No Participants]
{
"id": "invalid_property",
"code": 105,
"message": "The participants array cannot be empty.",
"url": "https://developer.layer.com/api.md#conversations",
"data": {
"property": "participants"
}
}
If User A
wants to talk to User B
, they should not need to create a new Conversation every time they talk. By reusing an existing Conversation, they can access the Message history and context around their previous communications. To help ensure that users do not inadvertantly create multiple Conversations when they intend to maintain a single thread of communication, Layer supports the notion of Distinct Conversations.
In a Distinct Conversation, it is guaranteed that among the given set of participants there will exist one (and only one) Conversation. Each Conversation has a distinct
property that determines how it is created. Possible values are:
true
If there is a matching Conversation, return it. Otherwise a new Conversation is created and returned.false
Always create and return a new Conversation.
An existing Conversation matches if it is itself Distinct, and it has the same participants.
A Distinct Conversation becomes a non-distinct Conversation if you change its participant list.
When creating a Distinct Conversation, there are three possible results.
201 (Created)
If there is no existing Distinct Conversation that matches the request. Create a new Conversation and return it. Result is the same as creating a non-distinct Conversation.
This feature is not available as of 6/22/2015.
If there is a matching Distinct Conversation, and one of these holds true, then an existing Conversation is returned.
- The
metadata
property was not included in the request - The
metadata
property was included but with a value ofnull
- The
metadata
property value is identical to the metadata of the matching Distinct Conversation
> 303 (See Other)
Location: /apps/{app_id}/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67
{
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/{app_id}/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"created_at": "2014-09-15T04:44:47+00:00",
"participants": [
"1234",
"777",
"999",
"111"
],
"distinct": true,
"metadata": {
"background_color": "#3c3c3c"
}
}
This feature is not available as of 6/22/2015.
If the matching Distinct Conversation has metadata different from what was requested, return an error that contains the matching Conversation so that the application can determine what steps to take next (e.g. use the Conversation or modify it).
> 409 (Conflict)
{
"id": "resource_conflict",
"code": 108,
"message": "The requested Distinct Conversation was found but had metadata that did not match your request.",
"url": "https://developer.layer.com/api.md#creating-a-conversation",
"data": {
"id": "layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"url": "https://api.layer.com/apps/{app_id}/conversations/f3cc7b32-3c92-11e4-baad-164230d1df67",
"created_at": "2014-09-15T04:44:47+00:00",
"participants": [
"1234",
"777",
"999",
"111"
],
"distinct": true,
"metadata": {
"background_color": "#3c3c3c"
}
}
}
GET /conversations/{conversation_id}
>> 200 (OK)
Conversation Representation
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You are no longer a participant in the specified Conversation.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Conversation with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#conversations"
}
When deleting a Conversation you can specify whether to delete or destroy the content as detailed above.
DELETE /conversations/{conversation_id}?destroy=true
>> 204 (No Content)
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You are no longer a participant in the specified Conversation.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Conversation with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#conversations"
}
This operation allows for small changes to the participant list, adding and removing a few users. Generally speaking, this operation is safer than replacing participants as there may be multiple participants attempting to change the participant list. Replacing the participant list would overwrite all other users efforts. Adding and removing participants has less risk of conflict.
PATCH /conversations/{conversation_id}
Content-Type: application/vnd.layer-patch+json
[
{"operation": "add", "property": "participants", "value": "user2"},
{"operation": "add", "property": "participants", "value": "user3"},
{"operation": "remove", "property": "participants", "value": "user1"}
]
>> 204 (No Content)
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You are no longer a participant in the specified Conversation.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Conversation with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Conversation has been deleted.",
"url": "https://developer.layer.com/api.md#conversations"
}
Attempting to add a participant who is already in a conversation, or remove one who is not, is considered a no-op, and does not return an error response.
Message Parts are returned inline if the size is less than 2 kilobytes. If the content is representable as UTF-8 string, it will be returned inline in the
body
property. If the content cannot be represented as a UTF-8 string, it will be returned in the body
property and a transfer_encoding
property
will be included with a value of base64
. For any parts over 2kb in size, a content
object will be included that points to the Rich Content associated
with the part.
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67",
"metadata_url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67/metadata",
"receipts_url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67/receipts",
"position": 15032697020,
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/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/7a0aefb8-3c97-11e4-baad-164230d1df67",
"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
}
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"user_id": "1234",
"name": "t-bone"
},
"is_unread": true,
"recipient_status": {
"777": "sent",
"999": "read",
"111": "delivered",
"1234": "read"
}
}
This is a paginated collection that is pre-sorted by position
descending.
GET /conversations/{conversation_id}/messages
>> 200 (OK)
Array of Message Representations
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You are no longer a participant in the specified Conversation.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Conversation with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Conversation has been deleted.",
"url": "https://developer.layer.com/api.md#conversations"
}
Sending a Message is accomplished by making a POST request to the /messages
resource as a complete JSON structure. Message parts whose bodies cannot be encoded as a JSON string need to be encoded as Base64, and the message part should contain an attribute called encoding
with the value base64
. Keep in mind that the un-encoded length of a message part cannot exceed 2KB. The server will decode such message parts before transmitting them to clients that can accept binary data.
POST /conversations/{conversation_id}/messages
{
"parts": [
{
"body": "Hello, World!",
"mime_type": "text/plain"
},
{
"body": "YW55IGNhcm5hbCBwbGVhc3VyZQ==",
"mime_type": "image/jpeg",
"encoding: "base64"
}
],
"push": {
"text": "This is the alert text to include with the Push Notification.",
"sound": "chime.aiff"
}
}
>> 201 (Created)
Message Representation
>> 204 (No Content)
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You are no longer a participant in the specified Conversation.",
"url": "https://developer.layer.com/api.md#conversations"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Conversation with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#conversations"
}
This feature is not available as of 6/22/2015.
POST /conversations/{conversation_id}/messages
{
"parts": [
{
"body": "Hello, World!",
"mime_type": "text/plain"
}
],
"metadata": {
"sending-device": "desktop",
"render-as-bold-for-users": {
"user8": "true",
"user13": "false"
}
}
}
A Message can be created with initial metadata using the optional metadata
property. See [Metadata] (#metadata) for rules on metadata
syntax.
The Layer platform provides extensive support for Push Notifications on both iOS (APNS) and Android (GCM). Notifications are delivered to devices for Messages sent via the Client API when keys are provided in the notification
dictionary. The values of these keys are detailed in the table below:
Key | Value Type | Description |
---|---|---|
text |
string | The text to be displayed on the notification alert. On iOS, displayed on the lock screen or banner. On GCM, delivered in the push intent as advisory information. |
sound |
string | The name of a sound to be played. On iOS, must exist in the main application bundle. On GCM, delivered in the push intent as advisory information. |
Note that values for iOS badge counts cannot be provided because the pushes are fanned out to all participants. You can enable support for server-side badge count management in the Layer Dashboard if you wish to provide badge counts for your iOS users.
Push Notifications are an optional feature and the notification
dictionary can be omitted entirely for Web-to-Web communication use-cases.
GET /messages/{message_id}
>> 200 (OK)
Message Representation
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You do not have access to the specified Message.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Message with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Message has been deleted.",
"url": "https://developer.layer.com/api.md#messages"
}
Regardless of which of the two flows above are used to send messages, messages are always delivered as JSON objects with message parts Base64 encoded if necessary.
When deleting a Message you can specify whether to delete or destroy the content as detailed above.
DELETE /messages/{message_uuid}?destroy=true
>> 204 (No Content)
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You do not have access to the specified Message.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Message with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Message has been deleted.",
"url": "https://developer.layer.com/api.md#messages"
}
Delivery and Read Receipts are used for two purposes:
- Notify other users that this user has received or has read the Message.
- Allows the UI to emphasize Messages that have not been read by the current user.
The Message's read state is stored in two Message properties:
- is_unread: This will be changed from true to false once a read receipt has been posted. This value only indicates whether the user associated with this session has read the message.
- recipient_status: This is a JSON hash of participants with a value of either "read" or "delivered" for each participant.
- Additionally, the Conversation.unread_message_count should decrement if the value has changed
Note that a Message that has been marked as read cannot be marked as unread. A delivery receipt on a Message that has already been marked as read will have no effect. A recipient_status value of "read" will never be changed to "delivered".
> POST /messages/{message_uuid}/receipts
{ "type": "delivery" }
{ "type": "read" }
This feature is not available as of 6/22/2015.
A required property message_id
should be provided which will write receipts up to the given identifier (inclusive) (according to position
).
> POST /conversations/{conversation_uuid}/messages/receipts
{ "type": "delivery" }
{ "type": "read", "message_id": "layer:///messages/0add69b0-6371-4f92-89da-81e202858d03" }
This feature is not available as of 6/22/2015.
The /messages/receipts endpoint enables marking of a number of Messages as read or as delivered. An array of Message IDs are provided (up to 100 per request) and these will be marked as read or delivered.
> POST /messages/receipts
{
"type": "delivery",
"message_ids": [
"layer:///messages/752edf2a-9374-466f-9f26-966c670ce781",
"layer:///messages/5c68c8b6-f703-4e4b-94a4-fd8d38996070"
]
}
{
"type": "read",
"message_ids": [
"layer:///messages/257abb93-ed98-4b14-98b9-ededf583bf61",
"layer:///messages/4a50d0fc-71bc-443e-8b8b-32f378abcc91"
]
}
All receipt endpoints share the common responses below; note however that the endpoints that update multiple Message objects will not return errors about individual Messages.
>> 204 (No Content)
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You do not have access to the specified Message.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "A Message with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#messages"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Message has been deleted.",
"url": "https://developer.layer.com/api.md#messages"
}
Metadata is a dictionary of string-key/string-value pairs that can be attached to Conversations and Messages. Metadata can be manipulated on a per-key basis via the PATCH
method or as an aggregate structure via the PUT
method. All Conversation and Message objects begin life with an implicit empty metadata dictionary ({}
).
Metadata keys are restricted to alphanumerics, underscores and hyphens (the regex /[\w_\-]+
). By default, metadata keys are visible to all participants that have access to the Conversation or Message upon which the data is anchored. This visibility can be changed by prefixing the key with either a single (_
) or a double underscore (__
) character. Keys prefixed with a single underscore are visible only to the user who set them and those prefixed with a double underscore are visible only to the device that set it. In the case of a Web Browser, a double underscore key will be visible to all other browser based sessions for a user but not to any mobile applications.
Metadata also supports the use of nested structures: the value can be either a string or a sub-dictionary keyed by strings with the same rules. Values for keys at arbitrary depth can be addressed as a JSON Pointer.
The following types of values are therefore acceptable within metadata:
- string value:
{"mykey": "hello"}
- subdictionary value:
{"mykey": {"age": "35", "profession": "developer"}}
The following therefore are NOT acceptable within metadata:
- Boolean:
{"mykey": true}
- Numbers:
{"mykey": 5}
- Arrays:
{"mykey": [1,2,3]}
If you need these types in your metadata, pass them as strings, and decode them on the receiving client:
- Arrays: {"mykey": "[1,2,3]"}`
The Layer API utilizes the JSON Patch document structure when manipulating subsets of the overall metadata document. The entire patchset is applied atomically and the response is a complete representation of the patched state of the metadata.
PATCH /conversations/{conversation_id}
PATCH /messages/{message_id}
Content-Type: application/vnd.layer-patch+json
[
{ "operation": "set", "path": "metadata.a.b.count", "value": "42" },
{ "operation": "add", "path": "metadata.a.b.myset", "value": "C" }
]
Note that the entire metadata structure can be replaced using:
PATCH /conversations/{conversation_id}
PATCH /messages/{message_id}
Content-Type: application/vnd.layer-patch+json
[
{
"operation": "set",
"path": "metadata",
"value": {
"a": "b",
"c": {
"d": "e"
}
}
}
]
The inclusion of Rich Content in a Message requires the upload of the content before a Message can be sent. Such messages will include a content
object with a download_url
in the Message structure in lieu of inline data.
Before sending a Message including one or more rich content parts, the client must initialize the content upload. This process provisions an authenticated, opaque URL that the client can then upload the content into. This API interaction looks like:
POST /content
Content-Length: 0
Upload-Content-Type: image/jpeg
Upload-Content-Length: 10000
Upload-Origin: http://mydomain.com
Body: Empty
The request includes the following additional headers:
Name | Description | Example |
---|---|---|
Upload-Content-Type |
Mime type for the content to be uploaded. | "image/png" |
Upload-Content-Length |
Size of the content to be uploaded | 10001 |
Upload-Origin |
For browsers that need CORS headers, provide the Origin for the request | javascript: window.location.origin; or "http://mychatapp.com" |
The response will either be a 201 (Created) response that includes a content
object or an error indicating why the operation failed:
>> 201 (Created)
Location: /content/3d0736d9-1a50-4e9a-a9b3-2400caa9e161
{
"id": "layer:///content/3d0736d9-1a50-4e9a-a9b3-2400caa9e161",
"download_url": null,
"expiration": null,
"upload_url": "https://www.googleapis.com/upload/storage/v1/b/myBucket/o?uploadType=resumable&upload_id=xa298sd_sdlkj2"
}
The URL specified via the upload_url
field in the body will be a Google Cloud Storage resumable upload URL. The specifics for completing the upload are detailed in the Google Cloud Storage JSON API Docs.
Once the Rich Content upload has completed, the client can send a Message that includes the Rich Content part:
POST /conversations/{conversation_id}/messages
{
"parts": [
{
"mime_type": "text/plain",
"body": "This is the message.",
},
{
"mime_type": "image/png",
"content": {
"id": "layer:///content/7a0aefb8-3c97-11e4-baad-164230d1df67",
"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
}
}
]
}
>> 201 (Created)
Message Representation
>> 422 (Unprocessable Entity) [Body Too Large]
{
"id": "invalid_property",
"code": 105,
"message": "The message could not be sent because it includes an inline content part greater than 2kb in size.",
"data": {
"property": “parts.body”
},
"url": "https://developer.layer.com/api.md#messages"
}
Messages that include Rich Content Message Parts will include an authenticated, expiring URL for downloading the content embedded in the Message representation:
{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67",
"metadata_url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67/metadata",
"receipts_url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67/receipts",
"position": 15032697020,
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/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/8c839735-5f95-439a-a867-30903c0133f2",
"download_url": "http://google-testbucket.storage.googleapis.com/testdata.txt?GoogleAccessId=1234567890123@developer.gserviceaccount.com&Expires=1331155464&Signature=BClz9e4UA2MRRDX62TPd8sNpUCxVsqUDG3YGPWvPcwN%2BmWBPqwgUYcOSszCPlgWREeF7oPGowkeKk7J4WApzkzxERdOQmAdrvshKSzUHg8Jqp1lw9tbiJfE2ExdOOIoJVmGLoDeAGnfzCd4fTsWcLbal9sFpqXsQI8IQi1493mw%3D",
"expiration": "2014-09-09T04:44:47+00:00",
"refresh_url": "https://api.layer.com/content/8c839735-5f95-439a-a867-30903c0133f2",
"size": 172114124
}
}
],
"sent_at": "2014-09-09T04:44:47+00:00",
"sender": {
"user_id": "1234",
"name": "t-bone"
},
"recipient_status": {
"777": "sent",
"999": "read",
"111": "delivered",
"1234": "read"
}
}
Every time you fetch a message from the server it will come with a new, fresh download url. The download URL for a particular content object can be refreshed by GET'ing the /content/{content_id}
resource of the content. The response will be a JSON document that includes newly refreshed URL for accessing the rich content:
GET /content/{content_id}
>> 200 (OK)
{
"id": "8a896e15-2908-4d7c-9f4e-d47e18ae2774",
"download_url": "http://google-testbucket.storage.googleapis.com/some/download/path",
"expiration": "2014-09-09T04:44:47+00:00",
"upload_url": null
}
>> 403 (Forbidden) [Access Denied]
{
"id": "access_denied",
"code": 101,
"message": "You do not have access to the specified Content.",
"url": "https://developer.layer.com/api.md#rich-content"
}
>> 404 (Not Found)
{
"id": "not_found",
"code": 102,
"message": "Content with the specified identifier could not be found.",
"url": "https://developer.layer.com/api.md#rich-content"
}
>> 410 (Gone)
{
"id": "object_deleted",
"code": 103,
"message": "The specified Content has been deleted.",
"url": "https://developer.layer.com/api.md#rich-content"
}
Implementation note: The GET request can only be issued if content has been uploaded. If Content was created on the REST server but never uploaded to storage, this request fails.
This feature is not available as of 6/22/2015.
Layer supports a rich domain model querying functionality that is useful for accessing communications data and driving user interfaces. This document details the structure of this querying interface within the Layer Messaging Protocol.
A Layer query object is initialized in terms of a single domain model that is to be queried. Without any additional constraints applied, such a query would match all objects of the specified type:
{
"model": "Conversation"
}
Queries can be further constrained by applying a predicate, sort descriptors, limit and offset. The result type can also be changed.
A Layer query can be executed with three possible result types:
object
- The query will return results as fully realized object representations.identifier
- The query will return results as object identifiers that refer to the objects that match the query.count
- The query will return an absolute count of the number of objects that match the constraints.
Note that all queries will return a count
in the response, and all responses will be constrained to a default paging size unless otherwise specified according to [Pagination] (#pagination).
{
"model": "Conversation",
"result_type": "identifier"
}
Default result type is object
.
Requests to the query endpoint will contain the following fields:
Name | Description | Example |
---|---|---|
model | The type of object being returned | Conversation |
result_type | The type of result_type being returned | identifier |
total_count | Total number of matching entires | 2345 |
results | Array of identifiers or objects. Ommited if result_type = "count" | ["layer:///conversations//e69e07fd-dab3-4e12-8305-f4f61fe5ec04"] |
Example:
{
“model”: “Conversation”,
“result_type": “object",
“total_count”: 1,
“results”: [{
"id": "layer:///messages/940de862-3c96-11e4-baad-164230d1df67",
"url": "https://api.layer.com/messages/940de862-3c96-11e4-baad-164230d1df67",
"position": 15032697020,
"conversation": {
"id": "layer:///conversations/e67b5da2-95ca-40c4-bfc5-a2a8baaeb50f",
"url": "https://api.layer.com/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/7a0aefb8-3c97-11e4-baad-164230d1df67",
"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
}
}
],
"push": {
"sound": "bleep.aiff",
"text": "This is a message."
},
"sent_at": "2014-09-09T04:44:47+00:00",
"received_at": "2014-09-16T19:54:39+00:00",
"sender": {
"user_id": "1234",
"name": "t-bone"
},
"is_unread": true,
"recipient_status": {
"777": "sent",
"999": "read",
"111": "delivered",
"1234": "read"
}
}]
}
Without a Predicate, all objects will be returned, except where [Pagination] (#pagination) is required.
A Layer Query is constrained using the Predicate. A Predicate defines a subset of the objects to return. A Predicate can be either
- an
and
,or
ornot
accompanied by Subpredicates - A Query Expression
A Query Expression is an expression used when querying for matching objects. It is an expression represented as a JSON object consisting of:
Key | Description |
---|---|
property |
Name of the property whose value will be tested |
operator |
The operator to use to test the property |
value |
A value to compare to the property |
operator
can be one of the following values:
==
: is_unread == true!=
: created_by != "fred"<
: unread_message_count < 5<=
: unread_message_count <= 5>
: unread_message_count > 5>=
: unread_message_count >= 5in
: "fred" in participantsnot_in
: "fred" not_in participants
value
can be one of the following value types:
- "t-bone" (string)
- 200 (integer)
- [ "blake", "t-bone", "kevin" ] (array)
- 1431644175852 (datetime)
- "1994-11-05T08:15:30-05:00" (datetime)
- "1994-11-05" (date)
- true (boolean)
The types that are acceptable for the value
vary on a per-model and property basis. Properties that specify datetimes can provide reference values as an integer specifying seconds since the epoch, a string containing a floating point value specifying millseconds since the epoch, or an ISO-8601 string specifying the exact time. When querying by date rather than datetime, use the "YYYY-MM-DD" format.
Example 1: Return all conversations whose participant list array contains the specified 3 participants.
{
"model": "Conversation",
"result_type": "object",
"predicate": {
"property": "participants",
"operator": "==",
"value": [ "blake", "t-bone", "kevin" ]
}
}
Note that queries on the participants property are treated as set comparisons; order does not matter.
Example 2: Return all conversations whose participant list contains the specified 3 participants:
{
"model": "Conversation",
"result_type": "object",
"predicate": {
"property": "participants",
"operator": "in",
"value": ["blake", "t-bone", "kevin"]
}
}
Note that this version of the query allows other participants to be present.
Example 3: Query for conversations that have unread_message_counts.
{
"model": "Conversation",
"result_type": "object",
"predicate": {
"property": "unread_message_count",
"operator": ">",
"value": 0
}
}
A full list of all properties that can be queried is listed in [Valid Query Properties] (#valid-query-properties).
There are two types of subproperties; queries into Hashes and queries into related objects.
The two most common Hashes used in queries are metadata
and recipient_status
.
You can query into hashes as deeply as needed by using "." notation to access subproperties.
{
"property": "metadata.title",
"operator": "=="
"value": "I am a title"
}
{
"property": "metadata.mydata.phone_drop_counter",
"operator": ">="
"value": 5
}
{
"property": "recipient_status.fred",
"operator": "=="
"value": "delivered"
}
The second type of subproperty is linked objects. This also uses "." notation to separate the subobjects. This example accesses a Conversation's last Message, and that last Message's MessageParts, and queries across all MessageParts in all last Messages for image/pngs. This should return an array of Conversations with suitable last_messages.
{
"property": "last_message.parts.mime_type",
"operator": "=="
"value": "image/png"
}
Querying by Date rather than by Date-Time is complicated by the fact that all Dates are stored in UTC time. To query on all messages sent on a given date, you can specify a timezone for your query using the Time-Zone
header. Timezone can be sent as an offset from GMT in minutes, or using [timezone names] (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
Passing timezone by minutes offset:
var d = new Date();
var offset = d.getTimezoneOffset();
xhr.setRequestHeader("Time-Zone", offset);
Passing timezone by name:
var timezoneName = Intl.DateTimeFormat().resolved.timeZone; // "America/Los_Angeles"
xhr.setRequestHeader("Time-Zone", timezoneName);
The Time-Zone header will have no effect on queries where a Datetime is used in your query, but will be used any time a Date is in your query.
Here are some examples of queries that can only be correctly handled using the Time-Zone header:
Query on messages sent at "2015-05-05" in your user's local timezone:
{
"property": "sent_at",
"operation": "==",
"value": "2015-05-05"
}
Query on messages sent on or after "2015-05-05" in your user's local timezone:
{
"property": "sent_at",
"operation": ">=",
"value": "2015-05-05"
}
Query on messages sent on "2015-05-05" or "2015-06-05" in your user's local timezone:
{
"property": "sent_at",
"operation": "in",
"value": ["2015-05-05", "2015-06-05"]
}
However, query by datetime will ignore the timezone:
{
"property": "sent_at",
"operation": "in",
"value": ["2015-05-05", "2015-05-08T04:44:47+00:00"]
}
The above query will use the Timezone header to find messages sent on "2015-05-05", but when evaluating the second value, will only return messages sent at exactly "2015-05-08T04:44:47+00:00".
Queries by ID support the "in" and "not_in" operator:
{
"property": "id",
"operator": "in",
"value": ["layer:///conversations/c1", "layer:///conversations/c2"]
}
This will return the two Conversations with the specified IDs.
One could also query for all messages from a conversation using "==", "!=", "in" or "not_in". This example queries on the Message's "conversation.id" property:
{
"model": "Message",
"predicate": {
"property": "conversation.id",
"operator": "==",
"value": "layer:///conversations/c1"
}
}
And this example queries on all Conversation's with a last_message whose sent_at fields fall in the given dates:
{
"model": "Conversation",
"predicate": {
"property": "last_message.sent_at",
"operator": "in",
"value": ["2015-05-05","2015-05-06"]
}
}
A Subpredicate is a Predicate that is a child of "and", "or" or "not".
Example 1: Query for conversations that include either "t-bone" or "blake" as participants and have unread messages:
{
"model": "Conversation",
"result_type": "object",
"predicate": {
"and": [
{
"property": "unread_message_count",
"operator": ">",
"value": 0
},
{
"or": [
{
"property": "participants",
"operator": "in",
"value": "t-bone"
},
{
"property": "participants",
"operator": "in",
"value": "blake"
}
]
}
]
}
}
In the above example, the "and" property defines an array of two subpredicates, one of which contains an "or" property defining two additional subpredicates.
Example 2: Query for conversations that include neither "t-bone" or "blake" as participants and have unread messages.
Note that while "and" and "or" contain arrays of subpredicates, "not" is always a single subpredicate.
{
"model": "Conversation",
"result_type": "object",
"predicate": {
"and": [
{
"not": {
{
"or": [
{
"property": "participants",
"operator": "in",
"value": "t-bone"
},
{
"property": "participants",
"operator": "in",
"value": "blake"
}
]
}
}
},
{
"property": "unread_message_count",
"operator": ">",
"value": 0
}
]
}
}
Layer Queries are sortable by supplying one or more sort fields to the query using the sort_descriptors
key. A sort field is added to sort_descriptors
using a name/value pair consisting of ASC
or DESC
and the property name.
Some objects support sorting across relationships. In this case, the sortable property is specified by concatenating the name of the relationship with the sortable property with a dot ('.') delimeter. An example of a relationship sort descriptor is "last_message.received_at".
{
"model": "Message",
"sort_descriptors": [
{"created_at": "asc"},
{"position": "desc"}
]
}
{
"model": "Conversation",
"sort_descriptors": [
{"last_message.received_at": "asc"}
]
}
Pagination rules for Queries are the same as described in the [Pagination] (#pagination) section, with one change: page_size can be provided in the JSON body or in the URL.
{
"model": "Conversation",
"page_size": 50
}
The following tables describe what properties are and are not valid to query on.
The Conversation Object:
Path | supported | Details | Operations |
---|---|---|---|
id | Yes | ==, !=, in, not_in | |
url | No | ||
unread_message_count | Yes | Find Conversations with unread messages | all |
metadata.title | Yes | Query on any metadata field | all |
distinct | Yes | Query to find distinct conversations between two participants. | ==, != |
last_message.is_unread | Yes | Query on any valid Message field | See Message Object Table below |
created_at | Yes | Query by Date or by DateTime | all |
participants | Yes | Equality tests are unordered tests | in, not_in, ==, != |
The Message Object:
path | supported | Details | Operations |
---|---|---|---|
id | Yes | ==, !=, in, not_in | |
url | No | ||
conversation.id | Yes | Allows querying for messages in a conversation | ==, !=, in, not_in |
conversation.url | No | ||
conversation.xxx | No | Querying on arbitrary Conversation properties from the Message table is not currently supported | |
is_unread | Yes | Query for unread (or read) messages | ==, != |
position | Yes | Query for all messages whose Position > xxx | all |
recipient_status.fred | Yes | Query for all messages read by the user with participant-id "fred" | ==, != |
sender.user_id | Yes | Query for all messages sent by a user | ==, != |
sender.name | Yes | Query for messages sent by the Platform API with a specific name | ==, != |
sent_at | Yes | Query by Date or by DateTime | all |
parts.mime_type | Yes | Query for all messages that contain images | ==, != |
parts.body | No | Not part of the v1.0 release plan | |
parts.encoding | No | Not part of the v1.0 release plan | |
parts.content_id | No | Not part of the v1.0 release plan |
Is there a demo app?