Generic document and collection access for private tokens on a server: read, list, count, create, update, delete, batch writes, file uploads and folders — nothing else: every platform operation on the project (provider switches, staff actions, the payment catalog, shipping, the traffic reports, the web address and its hosting) lives under Project. Paths are Firestore paths under store/{sid}/…, so this group can reach every entity CRM & Studio edits.
Image dimensions. An image upload (PNG, JPEG, GIF, WebP, SVG) answers with width and height on its entity, read from the file header on the server — on the original and on every variant, on both upload paths. Nothing to send; a format the parser does not size simply has no fields.
Large files. /admin/upload takes one request of at most 25 MB. Larger files (up to 1 GiB) go straight to storage: /admin/upload/start returns a Cloud Storage resumable session URI for one object path, you PUT the bytes to it in chunks (Content-Range: bytes a-b/total, 8 MiB each; 308 means send the next, 200/201 means done; after a network error an empty PUT with bytes */total tells you where to resume), and /admin/upload/finish registers the file exactly as a normal upload. /admin/upload/abort drops an unfinished one. The session URI is a write credential for that one file, valid 7 days; an unfinished session is discarded after 24 h. The SDK's Files.upload does all of this for you above 4 MiB (20 MiB before 2026-10-27). The one-request path is also bounded per API instance: when four such uploads are already being processed there, the fifth is answered 503 {error: "upload_busy"} with Retry-After: 2 before its body is read — the SDK retries it three times with a short back-off; a raw caller should do the same, or use the resumable path, which never meets that limit.
All operations are allowed (restricted by token). Should be exclusively used on backend.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to write value. /store/{storeId}/products/{productId}. When path is other then store/... store header or param is required. Path should have even number of parts - so it points to document. |
merge | boolean | optional | false - to override whole document. true by default - merge given data with original document. Default: true |
value | object | required | data to write into document in given path. |
Response
object — Returns result of write operation (confirmation).
Sample request
{
"path": "/store/{storeId}/data/en_US/products/tshirt-blue",
"value": {
"name": "Blue T-shirt",
"price": 24.9,
"tags": [
"apparel",
"new"
]
}
}Writes one document at path (merged into an existing one by default). Add merge: false to replace the whole document.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Optional. Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to write value. When path is other then store/... store header or param is required. Path should have even number of parts - so it points to document. |
mask | list | optional | Masks fields in response. Returns just subset of stored object. |
Response
object — Returns object for given path.
Sample request
{
"path": "/store/{storeId}/data/en_US/products/tshirt-blue",
"mask": [
"name",
"price"
]
}Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Optional. Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to write value. When path is other then store/... store header or param is required. Path should have even number of parts - so it points to document. |
value | object | required | data to write into document in given path. Use dot operator (param.field) to update data within objects. Use actions [union, remove], [increment], [update, delete] to edit document data. |
Response
object — Returns result of write operation (confirmation).
Sample request
{
"path": "/store/{storeId}/data/en_US/products/tshirt-blue",
"value": {
"price": 19.9,
"views": {
"item": 1,
"action": "increment"
},
"tags": {
"item": "sale",
"action": "union"
},
"meta.draft": {
"action": "delete"
}
}
}Plain values overwrite a field and dotted keys (meta.draft) reach into nested objects. An {item, action} object applies an operation instead - increment, union / remove for arrays, delete to drop the field.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Optional. Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to write value. When path is other then store/... store header or param is required. Path should have even number of parts - so it points to document. |
Response
object — Returns result of delete operation (confirmation).
Sample request
{
"path": "/store/{storeId}/data/en_US/products/tshirt-blue"
}Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to look up. When path is other then store/... store header or param is required. Path should have odd number of parts - so it points to collection. |
query | object | optional | Set of conditions to filter out documents. |
offset | string | optional | ID of last document. |
limit | integer | optional | Maximum number of documents per page. |
order_by | object | optional | Field name and direction - [asc, desc]. |
mask | list | optional | Masks fields in response. Returns just subset of stored object. |
Response
object — Returns list of objects for given path and query.
Sample request
{
"path": "/store/{storeId}/data/en_US/products",
"query": {
"category": "apparel",
"price": {
"<=": 50
}
},
"order_by": {
"price": "asc"
},
"limit": 20
}A scalar in query is an equality shorthand; an operator object such as {"<=": 50} applies that Firestore operator. Page by passing the last returned id as offset.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | list | required | List of paths where to look up. When path is other then store/... store header or param is required |
mask | list | optional | Masks fields in response. Returns just subset of stored object. |
meta | boolean | optional | true - the response also carries _meta: {[path]: {update_time}}, each found document's Firestore update time (nanosecond-exact ISO 8601). Pass it back as precondition.update_time to /admin/commit to fence a later write on the version you read. Default: false |
Response
object — Returns list of objects for given paths (keyed by path; plus _meta when meta is true).
Sample request
{
"path": [
"/store/{storeId}/data/en_US/products/tshirt-blue",
"/store/{storeId}/data/en_US/products/mug-white"
],
"mask": [
"name",
"price"
]
}Up to 100 preconditioned writes applied atomically (all or nothing). Every path must lie under the request's own store (store/{storeId}/…) and pass the token's path scope as a write. create fails when the document exists; set overwrites (with precondition: {exists: false} it is a create); update takes the same dotted keys and {item, action} operations as /admin/update; update and delete may be fenced on precondition.exists or on the exact update_time an earlier commit or /admin/batch {meta: true} returned. A refused precondition answers 409 {error: "precondition_failed", index, code} (code = ALREADY_EXISTS | FAILED_PRECONDITION) and nothing is written.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | required | The store every path belongs to (header or param). |
writes | list | required | List of at most 100 writes {path, op, value?, precondition?} — op is create | set | update | delete; path points to a document under store/{storeId}/; value is required for every op but delete; precondition is {exists?: boolean, update_time?: string}. |
Response
object — Returns {ok: true, results: [{path, update_time}]} — one entry per write, in request order; update_time is the document's new Firestore update time (the value a later precondition takes).
Sample request
{
"writes": [
{
"path": "/store/{storeId}/modules/echo/state/cursor",
"op": "create",
"value": {
"n": 1
}
},
{
"path": "/store/{storeId}/orders/o-1",
"op": "update",
"value": {
"modules.echo.synced": true
},
"precondition": {
"update_time": "2026-09-25T10:00:00.123456000Z"
}
}
]
}The first write creates a document (409 ALREADY_EXISTS when it is already there); the second patches an order only if it is still the version whose update_time was read back with /admin/batch {meta: true} (409 FAILED_PRECONDITION otherwise). Either failure leaves both untouched.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | string | required | Path where to write data. When path is other then store/... store header or param is required. Path should have odd number of parts - so it points to collection. |
value | object | required | data to write into document in given path. |
Response
object — Returns result of write operation (confirmation) - "id" and "ref" of new document
Sample request
{
"path": "/store/{storeId}/data/en_US/articles",
"value": {
"title": "Hello world",
"status": "draft"
}
}Appends a document with a generated id to the collection at path; the response carries the new id and ref.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | string | required | Path to a collection. When path is other then store/... store header or param is required. Path should have odd number of parts - so it points to collection. |
query | object | optional | Field-keyed map of conditions to filter documents before counting. Each value is an operator object like {"==": "ready"}, {">": 100}, or an array of them; see QueryObject helpers in core-lib. Omit for a total collection count. |
Response
object — Returns {count: number} — number of documents that match the query (or the total when no query was provided).
Sample request
{
"path": "/store/{storeId}/orders",
"query": {
"status": {
"==": "ready"
}
}
}Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Only when store is not included in header and path points to any other collection. |
path | string | required | Path to a collection (odd number of parts). Same rules as count. |
query | object | optional | Field-keyed map of conditions, exactly as for list / count. A value shaped {"_seconds": n, "_nanoseconds": n} (the way timestamps leave the API) is compared as a Firestore timestamp, so {"timestamp": {">=": {"_seconds": 1780000000, "_nanoseconds": 0}}} bounds a window. |
sum | list | optional | Numeric fields to total over the matching documents (dot paths allowed, e.g. invoice.total). A string is accepted for one field. |
avg | list | optional | Numeric fields to average over the matching documents. null in the result when no document carries the field. |
buckets | object | optional | A SERIES instead of one number - {field?: "timestamp", start: {"_seconds": n, "_nanoseconds": 0}, step?: 86400, count: 1..92} splits the aggregation into count consecutive buckets of step seconds from start on the timestamp field (which the query must then not filter itself; 400 buckets_invalid). This is how a chart gets its daily points - never by listing the window. |
Response
object — Returns {count, sum: {field: number}, avg: {field: number | null}} from ONE aggregation query — billed one document read per 1000 matching index entries, however many fields are aggregated. The rule for dashboard numbers: aggregate, never list-and-count. With buckets the answer also carries buckets: [{count, sum, avg}, ...] (one per bucket, in order) and mode, and costs 1 + min(n, count) reads for n matching documents - nothing more when the range is empty (mode: empty), the n documents listed once when they are fewer than the buckets (docs), else one aggregation per bucket (buckets).
Sample request
{
"path": "/store/{storeId}/orders",
"query": "{\"status\": {\"in\": [\"completed\", \"closed\"]}, \"timestamp\": {\">=\": {\"_seconds\": 1780000000, \"_nanoseconds\": 0}}}",
"sum": "[\"invoice.total\"]"
}Upload a file. Send multipart/form-data with a binary file part plus text fields (dir, name?, content_type?, sid?, id?) — or JSON with dir, name, content_type?, sid?, id? and base64-encoded data. The destination is store/{sid}/files/{dir}/{id} (up to 5 levels of folders). Missing folders are auto-created.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
dir | string | required | Relative directory under store/{sid}/files. Accepts "", "articles", "/articles/abc". Validated against //, .., trailing /, max depth 5, and segment charset [A-Za-z0-9_\-.]{1,64}. |
sid | string | optional | Target store id. Required when the request uses a master token (store='*' header); otherwise inferred from the auth context. |
id | string | optional | Document id under the resolved folder. Server mints a sortable id when omitted. |
name | string | optional | Original filename (drives extension). Defaults to the multipart file's name. |
content_type | string | optional | MIME type. Defaults to the multipart file's mimetype or application/octet-stream. |
variant | string | optional | Variant key (e.g. thumb, large). When set, the file is stored alongside the default and variants[<key>] is patched on the doc instead of replacing it. Requires id to point at an existing file. |
file | file | required | Binary file part (multipart/form-data field named file). Alternative to JSON data. |
data | string | optional | Base64-encoded file bytes (JSON shape only). Use the multipart file part for binary uploads instead. |
metadata | object | optional | Free-form key/value map (author, description, note, title, …). Stored on the default doc. In multipart, send as a JSON-stringified text field. |
keep_name | boolean | optional | Store the object under a slug of name instead of the minted id — store/{sid}/{dir}/urbanity-esg-report-2024.pdf (lower-case ASCII, dashes, ≤ 80 chars + the extension). The doc id is unaffected. A basename another object already holds gets a short random suffix (never overwritten); the same id re-uploaded with the same name replaces its object in place. Default upload only. In multipart, send as the text true. |
unzip | string | optional | Set to design to ingest the uploaded zip as a template bundle instead of storing it as a single file: entries are unzipped, filtered (docs/maps/php/dotfiles dropped), stored under the archive node's Storage prefix store/{sid}/{dir}/{id}/…, and ONE type=archive file doc (manifest: file list + extracted design signals) is written into the files tree at store/{sid}/files/{dir}/{id} — visible in the Files view and counted by the folder summaries. dir defaults to templates; a DesignAssetRef is upserted (replace-by-id, version bumped) into setup/design.assets. variant/metadata are ignored; returns {path, id, name, count, bytes, skipped, signals}. |
createBundle | boolean | optional | JSON shape only. Set to true to create an EMPTY asset bundle: writes a type=archive file doc with a zero-entry manifest at store/{sid}/files/{dir}/{id} (dir defaults to templates, id minted when omitted) and upserts its DesignAssetRef into setup/design.assets. Requires name. This is the bootstrap entry writes need (they 404 on a missing archive). Re-using an existing archive id is idempotent (keeps entries, refreshes name/ref). Returns {path, id, name, count, bytes, skipped, signals}. |
entry | string | optional | JSON shape only. Path of ONE entry inside an existing archive doc (id = the archive, dir defaults to templates) — overwrites/creates the object at the archive's storage prefix and upserts the manifest row, bumping the bundle's DesignAssetRef.version. Text entries get a short (5 min) cache-control so edits propagate. Requires base64 data (max 4 MB, extension must be in the bundle allow-list) — or remove: true to drop the entry instead. Returns {path, id, entry, size?, content_type?, removed?, version}. |
remove | boolean | optional | With entry: delete the entry (drops the manifest row; the files trigger reaps the Storage object) instead of writing it. |
Response
object — Returns {path, id, entity} — an image's entity carries width / height read from the file header (PNG, JPEG, GIF, WebP, SVG; the variant shape too) — the resolved Firestore doc path, the document id, and the stored FileEntity (or FileVariant for variant uploads). Every object is stored with Content-Disposition: inline; filename="<name>" (+ RFC 5987 filename* for non-ASCII), so a browser opens it in place but saves it under its uploaded name — /assets/** on the hosted sites forwards the header. With unzip=design, returns the ingest summary {path, id, name, count, bytes, skipped, signals} instead; with entry, the entry-write summary {path, id, entry, size?, content_type?, removed?, version}.
Sample request
{
"dir": "images",
"name": "pixel.png",
"content_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}The JSON shape - data is the file base64-encoded (here a 1x1 PNG). For real files send multipart/form-data with a binary file part plus the same text fields.
Start a resumable upload of a large file (above ~20 MB, up to 1 GiB) — the bytes go from the client straight to Cloud Storage, never through the API. Returns a Google Cloud Storage resumable session URI for ONE object path: PUT the file to it in chunks (Content-Range: bytes a-b/total, every chunk but the last a multiple of 256 KiB — session.chunk_bytes is the size to use; 308 = send the next chunk, 200/201 = done; after a network error PUT an empty body with Content-Range: bytes \*\/total and resume from the Range header), then call /admin/upload/finish. Same destination rules as /admin/upload (store/{sid}/files/{dir}/{id}, keep_name, replace in place with id); the object's content type, cache and disposition are fixed here. The session URI is a write credential for that one object for 7 days — keep it private. A pending session expires after 24 h.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
dir | string | required | Relative directory under store/{sid}/files — same validation as /admin/upload. |
name | string | required | Original filename (drives the extension and the download name). |
size | number | required | Exact file size in bytes (max 1073741824). Cloud Storage refuses an upload of any other length. |
content_type | string | optional | MIME type. Defaults to application/octet-stream. |
sid | string | optional | Target store id. Required when the request uses a master token (store='*' header); otherwise inferred from the auth context. |
id | string | optional | File document id. Server mints one when omitted; an existing file's id replaces its object in place (the current object stays intact until the new one is complete). |
keep_name | boolean | optional | Store the object under a slug of name instead of the id, as on /admin/upload. Two concurrent uploads of the same name never share an object (the later one gets a suffix). |
metadata | object | optional | Free-form key/value map stored on the file doc at finish. |
origin | string | optional | The browser origin the chunk PUTs will come from (location.origin) — Cloud Storage answers their CORS against it. Omit for server-side clients. |
Response
object — Returns {upload, id, path, storage_path, session: {uri, chunk_bytes, expires}} — upload is the session id finish / abort take, path the file doc finish will write.
Sample request
{
"dir": "video",
"name": "showreel.mp4",
"size": 734003200,
"content_type": "video/mp4"
}Finish a resumable upload: checks the object is complete (the declared size, a new object generation) and writes the file doc exactly as /admin/upload does — folder summaries, the files hook and the public URL follow as for any upload. 409 upload_incomplete = not all bytes arrived; resume the PUTs and call again. 404 upload_not_found after a lost response: read the file doc at the path start returned.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
upload | string | required | The session id /admin/upload/start returned. |
sid | string | optional | Target store id (master tokens). |
Response
object — Returns {path, id, entity} — an image's entity carries width / height, read back from the object's header — the same result as /admin/upload.
Sample request
{
"upload": "mhb2k1x0AbCdEfGhIjKl"
}Create an empty folder under store/{sid}/files. Auto-creates every missing ancestor (mkdir -p). Triggers maintain the summary aggregates.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
dir | string | required | Relative folder path. Same validation as /admin/upload's dir. Must not be empty (use a leaf segment). |
sid | string | optional | Target store id. Required for master-token requests; otherwise inferred. |
name | string | optional | Display name. Defaults to the leaf path segment. |
Response
object — Returns {path, type, name} of the leaf folder doc.
Sample request
{
"dir": "images/products",
"name": "Products"
}