reMarkable Cloud API

reMarkable Cloud API

This is an unofficial documentation for the cloud based sync service that comes with the reMarkable 1 tablet.

This document is based on an existing documentation 3, the existing implementation in rmapi 4 and observations made while implementing my own client 5.

The service is used to sync content from the tablet to a remote service and make it available on other devices. One can also upload new content to the cloud service and it will be synced to the device.

Disclaimer

This is a hobby project and not associated with the reMarkable company.

The API is a simple ReST oriented web service using JSON as a transfer format. The content of the notes is very close to the file based structure 2 which can be found on the tablet itself.

The API is split into separate parts:

  • Authentication

  • Storage

  • Notifications

Authentication

Users authenticate by logging into the reMarkable website and generating a one-time code. That code can be used to "register" a new device and request a Device Token from the API. That device token can then be used to request a User Token.

The user token is used to authorize requests against the API The user tokens expire after some time (less 24h?). The assumption is to generate a new user token for each session.

Host: https://my.remarkable.com

Endpoint

URL

Device Token

/token/json/2/device/new

User Token

/token/json/2/user/new

One Time Code

There are two URLs for generating the one time code:

A user must be logged in with his or her reMarkable account to generate a one time code.

The code is alphanumeric with 8 digits (e.g: apwngead).

The code can be used for one request, whether it is successful or not. Users can create as many codes as they want.

  • Not sure how long a code is valid.

  • Not sure in which way the .../desktop and .../mobile URLs are different.

Device Token

The device token can be requested by sending a POST request against the authentication API:

POST /token/json/2/device/new

with the following JSON body:

{
  "code": "apwngead",
  "deviceDesc": "desktop-windows",
  "deviceID": "d4605307-a145-48d2-b60a-3be2c46035ef"
}

The response is the token as a text/plain response body.

Unsuccessful requests contain a plain text error message in the response body.

Field

Description

code

the one time code

deviceDesc

enum, see below

deviceID

any UUID, generate one

Values for deviceDesc are:

  • desktop-windows

  • desktop-macos

  • mobile-android

  • mobile-ios

  • browser-chrome

  • remarkable

Not sure if the device description has any effect on the behavior of the service but invalid values are rejected by the API.

The deviceID is used in the notifications API, presumably to allow clients to recognize notifications that were caused by their own actions.

User Token

To request a user token send an empty POST request against the authentication API, using the device token as Authorization: Bearer header.

POST /token/json/2/user/new

On success, this will return the user token in text/plain. Again, the service returns error message as text/plain.

Discovery

The host for the Storage and Notification API is different from the authentication host. It is determined dynamically by requesting it from a "service manager".

Storage

GET https://service-manager-production-dot-remarkable-production.appspot.com/service/json/1/document-storage?environment=production&group=auth0%7C5a68dc51cb30df1234567890&apiVer=2

The URL parameters are as follows:

Name

Value

environment

production

group

auth0|5a68dc51cb30df1234567890

apiVer

2

Note

It seems that the group parameter does not have any effect. Responses are the same with or without that parameter.

The response looks like this:

{
    "Status": "OK",
    "Host": "document-storage-production-dot-remarkable-production.appspot.com"
}

The Status field can contain the text "OK" on success and an error message on failure.

This request is not authenticated.

Notifications

Discovery for the Notification API works the same as for storage, only the URL is different:

GET https://service-manager-production-dot-remarkable-production.appspot.com/service/json/1/notifications?environment=production&group=auth0%7C5a68dc51cb30df1234567890&apiVer=1

Returns:

{
    "Status": "OK",
    "Host": "08z1-notifications-production.cloud.remarkable.engineering"
}

Storage

The content model for the API is a tree structure which consists of "Notebooks" (leafs) and "Folders" (nodes). It resembles the structure of the folders and notebooks found on the tablet.

As with the tablet file system, the API returns a flat list and clients need to create the tree structure by looking at the references to parent id of each element.

Endpoint

Description

/document-storage/json/2/docs

List documents

/document-storage/json/2/upload/request

Prepare upload

/document-storage/json/2/upload/update-status

Update metadata

/document-storage/json/2/delete

Delete a document

The API only works on the metadata. The content (Notebooks, PDF documents, EPUB files) is handled separately by down- or uploading a ZIP archive.

The metadata is represented by the following JSON document:

{
    "ID": "0631045c-e3a9-45a0-8446-abcdef012345",
    "Version": 4,
    "Message": "",
    "Success": true,
    "BlobURLGet": "https://storage.googleapis.com/remarkable-production-document-storage/...[snip]",
    "BlobURLGetExpires": "2020-12-20T07:07:56.628298857Z",
    "ModifiedClient": "2020-12-12T22:16:39.539539Z",
    "Type": "DocumentType",
    "VissibleName": "My Notebook",
    "CurrentPage": 0,
    "Bookmarked": false,
    "Parent": "ec53580c-3579-4fe7-a096-012345abcdef"
}

Note that there is a typo in the VissibleName field.

Not all of the fields are needed in each request.

Error messages can bee included in a "200 OK" response if the Success field is set to false.

Example:

{
    "ID": "ec53580c-3579-4fe7-a096-fd1de8011b70",
    "Version":0,
    "Message": "Not found or access denied",
    "Success":false,
    "BlobURLGet":"",
    "BlobURLGetExpires":"0001-01-01T00:00:00Z",
    "ModifiedClient":"0001-01-01T00:00:00Z",
    "Type":"",
    "VissibleName":"",
    "CurrentPage":0,
    "Bookmarked":false,
    "Parent":""
}

For status codes other than 200 the error message seems to be sent as plain text.

Example:

Serving request failed, Msg: Authorization failed, invalid token: Could not validate jwt token: Token is expired, Origin: Authorization failed, invalid token: Could not validate jwt token: Token is expired, HTTPCode: 401

List Documents

The request is a GET on the list endpoint:

GET /document-storage/json/2/docs

It returns an array of metadata entries:

[
    {
        "ID": "ec53580c-3579-4fe7-a096-fd1de8011b70",
        "Version": 10,
        "Message": "",
        "Success": true,
        "BlobURLGet": "",
        "BlobURLGetExpires": "0001-01-01T00:00:00Z"
        ...
    },
    {...}
]

The BlobURLGet field is always empty when requesting the list. Other than that, the documents are complete.

Fetch a Single Item

There is no actual endpoint to request a single item. Instead, one fetches the list with a filter on an item's ID:

GET /document-storage/json/2/docs?doc=abcdef012345&withBlob=true

Parameters are:

Name

Description

doc

The ID of the requested item (folder or notebook)

withBlob

if present, the BlobURLGet field be set

The blob URL is used to retrieve a ZIP archive with the actual content. The archive contains all files that can be found on the tablet's file system:

<archive>
├── 04387b11-4fe3-41c5-9854-e0a5cbf463d5
│   ├── 2cb61608-9092-4c22-876c-b7ce0970ef4e-metadata.json
│   ├── 2cb61608-9092-4c22-876c-b7ce0970ef4e.rm
│   └── a370ab54-0e0f-4558-94d1-63c74d641c40.rm
├── 04387b11-4fe3-41c5-9854-e0a5cbf463d5.content
├── 04387b11-4fe3-41c5-9854-e0a5cbf463d5.lock
├── 04387b11-4fe3-41c5-9854-e0a5cbf463d5.metadata
├── 04387b11-4fe3-41c5-9854-e0a5cbf463d5.pagedata
└── 04387b11-4fe3-41c5-9854-e0a5cbf463d5.thumbnails
    ├── 2cb61608-9092-4c22-876c-b7ce0970ef4e.jpg
    └── a370ab54-0e0f-4558-94d1-63c74d641c40.jpg

The file system layout is described in the unofficial reMarkable wiki 2 in detail.

Authentication details for this call are already part of the URL and no additional authentication is needed.

The service will send error messages in XML format:

<?xml version='1.0' encoding='UTF-8'?>
<Error>
    <Code>AuthenticationRequired</Code>
    <Message>Authentication required.</Message>
</Error>

Update Metadata

To update the metadata for an item, send a PUT request to the update-status endpoint:

PUT /document-storage/json/2/upload/update-status

The request must include a JSON document with at least the following fields:

[
    {
        "ID": "0631045c-e3a9-45a0-8446-abcdef012345",
        "Version": 4,
        "ModifiedClient": "2020-12-12T22:16:39.539539Z"
    }
]

Note that we always need to send an array, although we update a single document.

  • The Version must be incremented by one, based on the last version that you have seen.

  • The ModifiedClient field should be set to the current time (UTC). Requests will still be processed even if the timestamp is not recent.

Additionally, set the fields you want to change:

Field

Description

VissibleName

Rename an item

Parent

Move an item to a different parent folder

Bookmarked

Bookmark an item (or remove a bookmark

When moving a CollectionType item, all its children are also moved. Specify an empty Parent to move an item to the root folder.

Warning

The API does not seem to validate the parent ID. Setting the parent to a non-existing ID will make the document appear in the root folder.

This should return a list of items with the updated status.

On success:

[
    {
        "ID": "25e3a0ce-080a-4389-be2a-f6aa45ce0207",
        "Version": 48,
        "Message": "",
        "Success":true
    }
]

On error:

[
    {
        "ID": "25e3a0ce-080a-4389-be2a-f6aa45ce0207",
        "Version": 48,
        "Message": "Version on server is not -1 of what you supplied: Server: 48, Client req: 48",
        "Success": false
    }
]

Upload a Document

Uploading documents like PDF or EPUB or notebooks is a multi-step process:

  1. Create an "Upload Request" (returns BlobURLPut).

  2. Use the BlobURLPut to upload a zip file with the content.

  3. Make another request to Update (rather: set) the metadata for the item.

/images/remarkable-api-upload.mermaid.svg

Sequence diagram with the steps required to upload a document.

If the upload for the zipped content fails, the item is not created (it cannot be deleted and apparently does not have to be deleted).

The first request is made against the upload request endpoint:

PUT /document-storage/json/2/upload/request

With some basic data as input:

[
    {
        "ID": "923812ec-ae98-40c3-bc59-e7ef065537ff",
        "Version": 1,
        "ModifiedClient": "2020-12-20T12:00:34.814814Z"
    }
]

The response (if successful) contains the upload URL:

[
    {
        "ID": "923812ec-ae98-40c3-bc59-e7ef065537ff",
        "Version": 1,
        "Message": "",
        "Success": true,
        "BlobURLPut": "https://storage.googleapis.com/remarkable-production-document-storage/...[snip]",
        "BlobURLPutExpires": "2020-12-20T12:20:48.434022276Z"
    }
]

Another request is made to upload content:

PUT {{BlobURLPut}}

The content is a Zip archive with the remarkable file format.

And a third request against the update endpoint to set the metadata:

PUT /document-storage/json/2/upload/update-status
[
    {
        "ID": "923812ec-ae98-40c3-bc59-e7ef065537ff",
        "Version": 1,
        "ModifiedClient": "2020-12-20T12:00:34.814814Z",
        "Type": "DocumentType",
        "VissibleName": "My Document",
        "Bookmarked": false,
        "Parent": "f86e2e28-234d-485d-ab34-fe1184571a92"
    }
]

Note

The Version field is set to 1, as this is a new item. The first request which created the upload URL does not create a "first version".

Delete an Item

To delete an item, send a PUT request with the metadata of the item to be deleted:

PUT /document-storage/json/2/delete

The request body needs the basic metadata of the document:

[
    {
        "ID": "81c58d3d-a550-4079-85c8-b581d7673390",
        "Version": 1,
        "ModifiedClient": "2020-12-20T12:00:34.814814Z"
    }
]

Deleting an item will set its parent to the special value trash.

If you delete a folder, only that folder's parent will be changed to trash. Any document contained inside the folder remains unchanged. It will become "invisible" though, as its parent element is no more accessible.

Notifications

The notifications host is obtained via the Discovery URL.

The endpoint is /notifications/ws/json/1 (found in this reMarkable github repository 6).

The complete URL might look like this:

wss://299t-notifications-production.cloud.remarkable.engineering/notifications/ws/json/1

The connection must be authenticated with a User Token in the Authentication: Bearer header.

Assumption: The notifications are one-way, meaning clients will will only receive and never send messages.

Messages

Messages are JSON objects an look like this:

{
  "message": {
    "attributes": {
      "auth0UserID": "auth0|123456abcdef130000000000",
      "bookmarked": "false",
      "event": "DocAdded",
      "id": "b083f079-c45e-4b1f-81ea-32c36a672142",
      "parent": "106ec061-d552-417b-a09c-e207cf87f597",
      "sourceDeviceDesc": "remarkable",
      "sourceDeviceID": "RM110-123-12345",
      "type": "DocumentType",
      "version": "2",
      "vissibleName": "My Notebook"
    },
    "messageId": "1804732942123456",
    "message_id": "1804732942123456",
    "publishTime": "2020-12-21T10:09:59.016Z",
    "publish_time": "2020-12-21T10:09:59.016Z"
  },
  "subscription": "projects/remarkable-production/subscriptions/sub-299t-notifications-production"
}

The message object consists of some metadata like messageId and publishTime. The actual message content is contained in the attributes property:

Name

Type

Description

auth0UserID

string

Seems to be the same as the one used in discovery; not sure what it means or why it is there

bookmarked

bool

true if the item is bookmarked

event

string

One of "DocAdded" or "DocDeleted"

id

string

The UUID of the item

parent

string

The UUID of the parent item

sourceDeviceDesc

string

These seems to be the device description from the registration process

sourceDeviceID

string

could be the ID of the device used during registration

type

string

"DocumentType" or "CollectionType"

version

int

The change version of the item

vissibleName

string

The name of the item, typo included

Note that all attributes are encoded as strings. One needs to parse the actual data type from that string.

The sourceDeviceID allows us to filter notifications caused by our own actions.

Events

We will receive notifications on the following events:

  • New notebook created

  • Notebook deleted

  • Notebook changed (bookmarked, renamed or moved)

  • Folder created

  • Folder deleted

  • Folder Changed (bookmarked, renamed or moved)

Creating items and updating items generates the same type of notification: DocAdded.

When an item is deleted, we will receive a notification with the DocDeleted type.

Note that the UI on the tablet does not actually delete items but moves them to a "Trash" folder. In that case the parent ID is set to the special value trash and we receive a DocAdded event.


1

https://remarkable.com/

2(1,2)

https://remarkablewiki.com/tech/filesystem

3

https://github.com/splitbrain/ReMarkableAPI

4

https://github.com/juruen/rmapi

5

https://github.com/akeil/rmtool

6

https://github.com/reMarkable/ws-stresser/blob/master/config.ini