Datastore HTTP API Documentation

Warning: The Datastore API has been deprecated. Learn more.

The Dropbox Datastore API supports synchronized storage of per-user structured data across multiple platforms and devices, such as app settings, bookmarks, or game state.

SDKs for using the Datastore API are provided for use in Objective C for iOS, in Java for Android, in JavaScript for use in web browsers, and in Python for use on servers.

In order to access the Datastore API in other languages or environments, you can call the HTTP endpoints used by the SDKs. The HTTP endpoints for the Datastore API behave similar to the HTTP endpoints for manipulating files in the Core API. In particular, you need an OAuth access token. You can find out more about OAuth in our OAuth guide. The rest of this page documents the endpoints specific to the Datastore API and their behavior.

General information

For a general introduction to datastores, see the Datastore API documentation for any of the supported client platforms. Important concepts referenced here include Datastore, Table, Record, Field, and List, which together form the data model used to discuss datastores and their operations here.

The term dbase64-encoded bytes refers to a string that encodes a sequence of bytes using a variant of the common urlsafe Base64 encoding, omitting trailing '=' signs and disallowing embedded whitespace. This encoding uses lowercase and uppercase letters, digits, hyphen, and underscore (the hyphen and underscore replace the plus and slash in the standard Base64 alphabet). The term dbase64 string refers to a string restricted to the same character set, but not necessarily forming a canonical encoding of a sequence of bytes.

API compatibility

This API will evolve. Future versions of this API may add new endpoints or parameters. In order to keep older clients working, the behavior and return value of APIs with given parameter values will not change from the currently documented behavior and return values, with two important exceptions: currently undocumented request parameters (whether they are actually ignored or not) may be given a specific meaning, and objects returned in responses may contain additional keys in the future.

Thus, clients that want to be future-proof should avoid passing undocumented parameters (as they may cause different behavior in the future), and they should avoid strict checks on the keys of objects found in responses.

For example, you should not consider a /list_datastores response invalid if it contains additional keys in the <dsinfo> or <infodict> in addition to the keys currently documented below for these objects.

Shared datastores

Apps may want to share data between users. With the Datastore API, apps can share a datastore across multiple Dropbox accounts. The unit of sharing is a single datastore, and one or more datastores may be shared between accounts. Any datastore with a shareable ID (see "Datastore identifiers" below) can be shared by assigning roles to principals, creating an access control list. Any Dropbox account with the correct permissions will then be able to open the shared datastore by ID.

There are two available principals to whom you may apply a role:

  • Public – The role will apply to all Dropbox users.
  • Team – The role will apply to everyone on the user's team (only applicable for Dropbox for Business accounts).

There are four available roles:

  • None – The principal has no access to this datastore.
  • Viewer – The principal is able to view this datastore.
  • Editor – The principal is able to edit this datastore.
  • Owner – The principal is the owner of this datastore. This role cannot be assigned directly. The user who created a datastore is always that datastore's owner.

Datastore identifiers

An individual datastore has a few different types of identifiers that are used for different purposes. Apps typically use a subset of identifiers, but the useful ones will vary by use case.

  • ID – The datastore's ID is a string that uniquely identifies the datastore for a given user-app pair across all the user's devices. There are actually two types of datastore IDs:

    • Private IDs – These are IDs which are meaningful to the developer of the app, such as "default" or "settings". The scope of private IDs is the current user-app pair. These IDs are 1-64 characters containing only lowercase letters, digits, dot, hyphen or underscore, and they must not begin or end with dot. To create a datastore with a private ID, use the get_or_create_datastore endpoint.
    • Shareable IDs – These are IDs are not only unique for the user-app pair, they're also unique across Dropbox. These are more appropriate when treating each datastore as an individual document where users may create an unknown number of them. A shareable datastore ID is a dot followed by a dbase64 string of 1-63 characters. To create a datastore with a shareable ID, use the create_datastore endpoint. Note that once a shareable ID has been used, it cannot be issued again, even if deleted.

    Note: Some outdated datastore SDKs enforce a limit of only 32 characters for private datastore IDs. To interoperate with these SDKs, consider constraining your IDs to the smaller 32-character limit.

  • Handle – A datastore also has a handle which is a string used as an internal identifier, assigned to it by the server. Handles are managed internally in the Datastore SDKs but code that uses the HTTP endpoints directly must pass the handle as a parameter for API calls that read or write to a datastore. A handle is a dbase64 string of 1-1000 characters. When a datastore with a private ID is deleted and recreated, it is assigned a new handle.

  • Title – A title is stored as part of the datastore metadata (explained in more detail below). The title is optional and should be used as the human readable name of a datastore. Unlike IDs and handles, the title can be edited after the datastore has been created. It can also be accessed without having to first load the entire datastore, making it particularly useful for labels in a list of datastore-based documents.

Changes and values

The objects described in this section are all represented as JSON in the HTTP API. A formal grammar is included in the next section, but first we introduce the concepts.

A snapshot refers to the state of a datastore at a given point in time. It refers to a specific set of tables, records, and fields that make up the content of the datastore at that time.

The revision is an integer indicating a specific version or snapshot of a datastore. Each time the server successfully accepts a put_delta request it increments the revision by one (or occasionally more). A newly created datastore has revision 0; when a private datastore is deleted and recreated its revision is reset to 0.

A delta describes a list of changes to a datastore. A delta has an associated revision, which identifies the snapshot to which the delta should be applied. A delta also has an optional nonce field. This is different from a cryptographic nonce; it is simply a string used to distinguish deltas originating from the current client from deltas sent by other clients. (This is useful in case the server accepted a delta but the client didn't receive the 'success' response due to some failure.)

A change describes a record-level operation; it can be a record insertion, a record update, or a record deletion. Record insertions and updates can reference multiple fields. A change refers to a record by its table ID and record ID. An insertion specifies the initial contents of the record as a dictionary mapping field names to values. An update specifies the changes as a dictionary mapping field names to operations.

Table IDs, record IDs, and field names are all strings of 1-64 characters from the following set: lowercase and uppercase letters, digits, dot, hyphen, underscore, plus, slash, equal. Reserved IDs/names starting with colon are also allowed; the general form of these is a colon followed by 1-63 characters from the above set. The server currently only accepts the reserved table IDs ':info' and ':acl', but it may start returning and/or accepting other reserved IDs/names in the future.

Note: Some datastore SDKs enforce a limit of only 32 characters for table IDs, record IDs, and field names. To interoperate with these SDKs, consider constraining your IDs and field names to the smaller 32-character limit.

An operation represents a change to a single field. Whole-field operations can PUT a new value into a field (either creating the field or replacing a previous value) or DELETE a previously existing field. List operations come in five flavors. LIST_CREATE creates a field containing an empty list value. The remaining four flavors require an existing field whose value is a list:

  • LIST_PUT replaces an existing list item with a new atomic value
  • LIST_INSERT inserts a new atomic value into a list
  • LIST_DELETE deletes an existing list item
  • LIST_MOVE moves an existing list item to a new position

These four all take a 0-based index indicating the position for the value to insert/replace/delete; LIST_PUT and LIST_INSERT additionally take an atomic value; LIST_MOVE takes a second position. All indexes must be >= 0. For LIST_INSERT, the index must be less than or equal to the length of the list; for all other operations the index must be strictly less than the length of the list.

A value is either a list of atoms or a single atom. Lists cannot contain other lists, only atoms.

An atom can be a primitive value of one of the following types:

  • integer (64-bit signed)
  • floating point (64-bit IEEE double precision, including NaN and ± infinity)
  • Boolean
  • string (UTF-8-encoded Unicode code points)
  • bytes (sequence of arbitrary 8-bit bytes)
  • timestamp (milliseconds since 1/1/1970 UTC)

There's no size limit on a field inside the record, however the overall record is limited to 100 x 1024 bytes (a.k.a. 100 KiB).


Datastores may have metadata associated with them which, if present, will be returned by list_datastores in the <infodict>. Metadata is represented in the datastore itself in the :info table, as fields of the info record. The server restricts updates to this table to known metadata items. Currently defined metadata items are:

  • title
    An optional human-readable title for the document. This does not need to be unique across datastores.
  • mtime
    An optional timestamp giving the date/time of the last modification. It is up to the app to decide when to set this and to what value.

Access control

Shareable datastores can include an :acl table, which represents the access control list for the datastore. Each record in this table maps a principal to a role. The record ID names the principal (either public or team), and the role field is an integer mapping to a predifined role: 1000 for viewer and 2000 for editor.

Some calls, such as list_datastores return the authenticated user's effective role. In this case, the role can be 1000 or 2000 as above, but also 3000, which indicates that the user is the owner of the datastore. The creator of a datastore is always that datastore's owner, so the owner role cannot be assigned.

Storage size limits

Datastores have limits on their maximum size to ensure good performance across platforms. You should keep these in mind as guidelines when modeling your datastores.

Your app can store up to 5MB of data across all its datastores without counting against the user's storage quota. Any data beyond the first 5MB is factored into the user's Dropbox storage quota, and writing can be limited in these cases when a user is over quota. Sizes are calculated as:

  • The size of a datastore is calculated by summing the size of all records, plus 1000 bytes for the datastore itself.
  • The size of a record is calculated by summing the size of all values in all fields, plus 100 bytes for the record itself.
  • The size of a field is a fixed 100 bytes for the field itself plus:
    • for string or bytes values, the length in bytes of the value.
    • for List values, 20 bytes for each list element plus the size of each element.
    • for all other types, no additional contribution.
  • The size of changes made in a put_delta operation is calculated by summing the size of each change, plus 100 bytes for the delta itself. The size of each change is calculated by summing the size of the values contained in the change (for field updates and list put and insert operations) plus 100 bytes for the change itself.

Formal grammar for deltas and snapshots

Here is a formal grammar describing the details of how deltas and snapshots are encoded as JSON. Please refer to the previous section for the semantics. The grammar has multiple roots, referenced from various API endpoints described in later sections.

Notes on the notation:

  • A hash sign (#) introduces a comment.
  • [...] specifies a JSON list, not an optional grammar production. Lists with a variable number of items are shown as [<item>, ...]. These may be empty.
  • {...} specifies a JSON dictionary. The key order is not constrained by the grammar. Dictionaries with a variable number of items are shown as {<key>: <value>, ...}. These may be empty.

Here is the grammar:

<list_of_deltas>    ::= [<delta>, ...]  # ordered by rev
<delta>             ::= {"rev": <rev>, "changes": <list_of_changes>, "nonce": <dbase64>}
                        # nonce is optional
<list_of_changes>   ::= [<change>, ...]  # in the order in which they should be applied
<change>            ::= ["I", <tid>, <recordid>, <datadict>]  # INSERT
                      | ["U", <tid>, <recordid>, <opdict>]  # UPDATE
                      | ["D", <tid>, <recordid>]  # DELETE
<datadict>          ::= {<field>: <value>, ...}
<opdict>            ::= {<field>: <fieldop>, ...}
<field>             ::= <id>
<tid>               ::= <id>
<recordid>          ::= <id>
<id>                ::= <str>  # see constraints above
<fieldop>           ::= ["P", <value>]  # PUT
                      | ["D"]  # DELETE
                      | ["LC"] # LIST_CREATE
                      | ["LP", <index>, <atom>]  # LIST_PUT
                      | ["LI", <index>, <atom>]  # LIST_INSERT
                      | ["LD", <index>]  # LIST_DELETE
                      | ["LM", <index>, <index>]  # LIST_MOVE
<index>             ::= <int>
<value>             ::= <atom>
                      | [<atom>, ...]
<atom>              ::= <Boolean>
                      | <str>  # always means UTF-8-encoded text string
                      | <number>  # always means floating point
                      | <wrapped_int>  # must be used to represent integers
                      | <wrapped_special>  # used for NaN and infinities
                      | <wrapped_timestamp>
                      | <wrapped_bytes>
<wrapped_int>       ::= {"I": <str>}  # decimal representation of a signed 64-bit int
<wrapped_special>   ::= {"N": "nan"}
                      | {"N": "+inf"}
                      | {"N": "-inf"}
<wrapped_timestamp> ::= {"T" : <str>}  # decimal representation of a signed 64-bit int
<wrapped_bytes>     ::= {"B" : <dbase64>}  # dbase64-encoded bytes
<rev>               ::= <int>
<handle>            ::= <dbase64>
<dbase64>           ::= <str>  # dbase64 string
<number>            ::= <int>
                      | <float>
<Boolean>           ::= # standard JSON Boolean (false or true)
<str>               ::= # standard JSON string
<int>               ::= # standard JSON integer
<float>             ::= # standard JSON float
<role>              ::= 1000 # viewer
                      | 2000 # editor
                      | 3000 # owner

Standard error responses

Most invalid requests (e.g. for invalid parameter values) return an HTTP status of 400 or 404; authentication errors return an HTTP status of 401 or 403.

Some application-level errors return an HTTP status of 200 with a JSON-encoded dictionary in the body. These include:

  • If a datastore is not found or access to it is not allowed to the requesting user, a JSON-encoded dictionary is returned of the form <notfound_result> (see below).
  • If the put_delta endpoint detects a conflict, it returns a JSON-encoded dictionary of the form <conflict_result> (see below).

These results are defined as follows:

<notfound_result>  ::= {"notfound": <str>}  # error message
<conflict_result>  ::= {"conflict": <str>}  # error message
<access_denied_result> ::= {"access_denied": <str>} # error message

The error messages are designed to be understandable for developers but not suitable for end users.


With webhooks, your web app can be notified when your users' datastores change. You'll need to specify a webhook URL in the App Console. To learn more about how to use webhooks, read the webhooks tutorial.

Notification format

For datastore changes, your endpoint will receive JSON with the following format:

    "datastore_delta": [
            "handle": "abc123",
            "dsid": "default",
            "change_type": "update",
            "owner": 12345678,
            "updater": 23456789
            "handle": "456xyz",

The following datastore change_types are defined:

  • update – The contents of the datastore changed. (I.e. records were inserted, deleted, or modified.)
  • create – The datastore was created.
  • delete – The datastore was deleted.



Retrieves a list of all datastores for the current user.
URL structure

<list_datastores_result> ::= {"datastores": <datastores>, "token": <dbase64>}
<datastores>             ::= [<dsinfo>, ...]
<dsinfo>                 ::= {"dsid": <str>, "handle": <handle>,
                              "rev": <rev>, "info": <infodict>,
                              "role": <role>}
<infodict>               ::= {"title": <str>, "mtime": <wrapped_timestamp>}

The <infodict> dict reflects the contents of the datastore's metadata and is omitted if no metadata is present in the datastore; even if present, individual keys are omitted if the corresponding metadata field is not set in the datastore.

The role field is only present for shareable datastores.

The token is a dbase64 string which represents a hash of the <datastores> list, exclusive of the rev and mtime values. It can be passed to the await endpoint.


Checks the validity of a datastore ID and returns its revision and handle. The ID must refer to an existing datastore. This works for both private and shareable datastore IDs.
URL Structure
the datastore ID to check

<get_result> ::= {"rev": <rev>, "handle": <handle>, "role": <role>}

Note: The role field is only returned for shareable datastores.




Checks the validity of a private datastore ID or creates a new datastore with the given ID, and returns its revision and handle. This only works for private datastore IDs. To create a datastore with a shareable ID, use create_datastore.
URL Structure
string giving the datastore ID

<get_or_create_result> ::= {"rev": <rev>, "handle": <handle>,
                            "created": <Boolean>}


Creates a new datastore with a shareable ID and returns its revision and handle. If the datastore already exists and is owned by the same user, this call returns its revision and handle. If the datastore already exists and is owned by another user, this call returns an error. To open an existing shared datastore, use get_datastore with the shareable ID. To create a datastore with a private ID, use get_or_create_datastore.
URL Structure
string giving a new shareable datastore ID
dbase64 string to be used as a "seed" for the datastore ID

The key and dsid parameters must satisfy the following relationship:

dsid == '.' + dbase64(sha256(key))

Note that the SHA-256 hash is taken directly from the dbase64 key string (the key is not decoded to a raw string of bytes), and the datastore ID is formed by dbase64-encoding the bytes of the SHA-256 hash digest.


<get_or_create_result> ::= {"rev": <rev>, "handle": <handle>,
                            "created": <Boolean>, "role": <role>}




Remove a datastore. Once this operation is executed successfully all future references to the given handle will return a <notfound_result> error.
URL Structure
the handle of an existing datastore

<delete_result> ::= {"ok": <str>}  # confirmation message




Return deltas for a datastore since a given revision.
URL Structure
the handle of an existing datastore
the revision from which to start

<get_deltas_result> ::= {"deltas": <list_of_deltas>}

If no deltas newer than the rev parameter are known to the server, this will return an empty list. If the full response would exceed the size limit not all deltas known to the server will be returned: older deltas (i.e. with lower revisions) will be returned first, and at least one delta will be returned.




Write a delta to the server, if the client is up to date.
URL Structure
the handle of an existing datastore
the revision to which to apply the delta
optional dbase64 string (up to 100 characters) used to uniquely identify this delta
JSON-encoded <list_of_changes>

<put_result> ::= {"rev": <rev>}




Return a full snapshot of a datastore.
URL Structure
the handle of an existing datastore

<snapshot_result> ::= {"rows": <list_of_rows>, "rev": <rev>, "role": <role>}
<list_of_rows>    ::= [<row>, ...]
<row>             ::= {"tid": <tid>, "rowid": <recordid>, "data": <datadict>}

Note: This response uses row(s) instead of record(s).




This is a "long poll" request that blocks up to a minute or until a change is detected.
URL Structure
optional <get_deltas_arg> (see below)
optional <list_datastores_arg> (see below)

<get_deltas_arg>      ::= {"cursors": <cursordict>}
<cursordict>          ::= {<handle>: <rev>, ...}
<list_datastores_arg> ::= {"token": <dbase64>}


<await_result>         ::= {"get_deltas": <await_deltas_result>,
                            "list_datastores": <list_datastores_result>}
<await_deltas_result>  ::= {"deltas": <datastores_map>}
<datastores_map>       ::= {<handle>: <per_datastore_result>, ...}
<per_datastore_result> ::= <get_deltas_result>
                         | <notfound_result>

The "get_deltas" and "list_datastores" keys in the top-level result are optional:

  • "get_deltas" is present if any of the datastores whose handle was specified in the request has a change or is invalid or deleted. In this case the <datastores_map> has a <get_deltas_result> entry for every such handle that has a new revision. In addition, for handles that are invalid or deleted, it has a <notfound_result> entry.
  • "list_datastores" is present if a non-empty token was specified in the request and a list_datastores request would return a different token. In that case the full <list_datastores> response is returned.

The request returns when either of these conditions is true, or if approximately a minute passes. In the latter case an empty dictionary is returned.