Skip to content

Instantly share code, notes, and snippets.

@blakewatters
Last active November 22, 2018 07:29
Show Gist options
  • Save blakewatters/1e1da150117df226be49 to your computer and use it in GitHub Desktop.
Save blakewatters/1e1da150117df226be49 to your computer and use it in GitHub Desktop.
Layer_REST_API.md

Layer REST API

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.

Status

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

The following list of features indicates the following:

  1. done: Feature is implemented and ready to use
  2. planned: Feature is schedule to be done soon
  3. future: Feature will be done after the first GA release
  4. redesign: The feature was recently redesigned; please check if this breaks your implementation
  5. new: New feature added to spec
  6. deprecated: 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
  • [Conversations] (#conversations)
    • [Listing Conversations] (#listing-conversations): done
      • [Pagination] (#pagination): planned
    • [Retrieving a Conversation] (#retrieving-a-conversation): done
    • [Creating a Conversation] (#creating-a-conversation): done
      • [Distinct Conversations] (#distinct-conversations): done redesigned
    • [Deleting A Conversation] (#deleting-a-conversation): done
    • [Add/Remove Participants] (#addremove-participants) done
    • [Metadata] (#metadata)
      • [Patching Metadata Structures] (#patching-metadata-structures): done
    • [Writing Receipts] (#writing-receipts-for-all-messages-in-a-conversation): future
  • [Messages] (#messages)
    • [Listing Messages in a Conversation] (#listing-messages-in-a-conversation): done
      • [Pagination] (#pagination): planned
    • [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
    • [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
    • [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
  • [Querying] (#querying) planned

API Overview

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.

API Versioning

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

Schema

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
Summary & Detail Representations

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.

Parameters

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

Client Errors

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"
    }
}
Error Attributes
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" }
Error Responses
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

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.

De-duplication

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.

Hypermedia

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.

Pagination

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.
Implementation note: The `next` and `prev` links can be implemented by including the ids of the last and first items in the page as shown above. The server will need to perform a query that orders the list, finds the location of the id, and returns the page relative to that id. Here's a PostgreSQL query to do this. Let’s assume that we have a table `t` with columns `id`, `datavalue` and `sortfield`, the last being the field(s) we want to order the table by. Let the page size by `p`. Let’s say we want the page starting after `id` = `x` based on the order in `sortfield`. This PostgreSQL query should return the page.
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.

Conditional Requests

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.

Resource Mutability

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

Destroying Content

When deleting content within the Layer API you have the option of deleting or destroying the content.

  1. delete: The content is deleted from all of the clients associated with the authenticated user.
  2. 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.

Destroy the content

DELETE /conversations/{conversation_id}?destroy=true

DELETE /messages/{message_id}?destroy=true

Delete the content

This feature is not available as of 6/22/2015.

DELETE /conversations/{conversation_id}?destroy=false

DELETE /messages/{message_id}?destroy=false

REST Endpoints

This section details the individual resources that make up the API.

Common Responses

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

Root Request

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.

Authentication

Obtaining a Nonce

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

Authenticating with an Identity Token

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

Link Header

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

Authentication Challenges

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)
[]

Ending a Session

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

Conversations

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

Listing Conversations

GET /conversations

>> 200 (OK)
Array of Conversation Representations

Paging of Conversations is not available as of 6/22/2015.

Creating a Conversation

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

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.

Handling Metadata with Distinct Conversations

When creating a Distinct Conversation, there are three possible results.

1. Conversation Created
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.

2. Conversation Found

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.

  1. The metadata property was not included in the request
  2. The metadata property was included but with a value of null
  3. 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"
    }
}
3. Conflict Found

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

Retrieving a Conversation

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

Deleting a Conversation

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

Add/Remove Participants

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.

Messages

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

Listing Messages in a Conversation

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

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"
}
Sending a Message with Metadata

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.

Push Notifications

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.

Retrieving a Message

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.

Deleting a Message

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 & Read Receipts

Delivery and Read Receipts are used for two purposes:

  1. Notify other users that this user has received or has read the Message.
  2. 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:

  1. 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.
  2. recipient_status: This is a JSON hash of participants with a value of either "read" or "delivered" for each participant.
  3. 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".

Writing a Receipt for a Message

> POST /messages/{message_uuid}/receipts

{ "type": "delivery" }

{ "type": "read" }

Writing Receipts for all Messages in a Conversation

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

Writing Receipts for Specific Messages

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

Responses for Receipts

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

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

Patching Metadata Structures

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

Rich Content

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.

Initiating a Rich Content Upload

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.

Sending a Message including Rich Content

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

Downloading a Rich Content Message Part

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

Refreshing the download URL for a Content Object

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.

Querying

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.

Query Objects

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.

Query Result Types

A Layer query can be executed with three possible result types:

  1. object - The query will return results as fully realized object representations.
  2. identifier - The query will return results as object identifiers that refer to the objects that match the query.
  3. 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.

Query Responses

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

Predicates

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 or not accompanied by Subpredicates
  • A Query Expression
Query Expressions

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 >= 5
  • in: "fred" in participants
  • not_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).

Subproperties

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

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

Querying by ID

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

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

Sort Descriptors

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

Query Pagination

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
}

Valid Query Properties

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
@EricHoangNV
Copy link

If you modify the REST demo, set value of sampleConversation.url with URL of any conversation that being used by app (in my case, Android sample app - Layer Quick Start), set USER_ID as "Dashboard" then you should be able to send/receive messages from that conversation. In other words, use REST to chat between web & android.

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