Write rows with the table API
Push rows into a Spatial Map table from your own code: API keys, the 12 REST endpoints, bearer auth, bulk upsert, filters and the 60-requests-a-minute limit.
How the table API works
Every table gets a REST API automatically. Your systems — a facilities-management export, an RTLS gateway, a nightly script — send rows to it with an API key, and every map that shows the table through a data link updates on its own.
- One key, one table. The key itself says which table and workspace a request writes to, so neither appears in the URL.
- Rows are identified by `entityKey` — your stable ID for the row, such as a room name or an asset tag. Writing the same key again updates that row.
- Every write is recorded. Each insert, update and delete adds a step to the row's history, which the Records grid and the viewer's table history read.
Create an API key
Open the table, then the API & access tab. The API keys card: "Each key can write to this table only. Give every system its own key so you can revoke one without breaking the rest."
- 1Name the key
Type a name in "Key name, e.g. warehouse-scanner" — for example `room-status-export`.
- 2Create key
Your new API key appears: "Copy it now and store it somewhere safe — it won't be shown again." Keys start with `sgv_tbl_`.
- 3Click "I've saved it"
The key joins the list as Active, with when it was created and when it was last used ("never used" until the first request).
To cut a system off, click Revoke and confirm Revoke key. Rows it already wrote stay; its next request gets `401`. To rotate: create a new key, deploy it, then revoke the old one.
Base URL and authentication
The API is served from the same address as the app. The Endpoints card shows the Base URL and the Auth header with copy buttons; on Spatial Map's hosted app the full base URL is:
https://app.spatialmap.io/api/v1/public/tables
Authorization: Bearer sgv_tbl_…- Send `Content-Type: application/json` with every body.
- A missing, revoked or unknown key returns `401` ("invalid token") — the API doesn't say which.
- No workspace header is needed: the key already identifies the workspace and table.
The 12 endpoints
Paths are relative to the base URL. Each endpoint on the Endpoints card expands to a curl example you can copy, filled in with your table's first fields.
| Method and path | What it does |
|---|---|
| `POST /rows` | Insert or update one row (upsert by `entityKey`). The `payload` you send replaces the stored one. |
| `POST /rows/bulk` | Upsert up to 1,000 rows in one call. Invalid rows are reported in `errors`; valid rows still commit. |
| `GET /rows` | List rows, paginated: `page`, `pageSize` (default 100, max 500), `sortBy` (`entity_key`, `ts` or `updated_at`), `sortDir` (`asc` or `desc`). Returns `totalCount`. |
| `POST /rows/query` | Filter and paginate with a filter tree; add `"includeCount": true` for `totalCount`. |
| `GET /rows/{key}` | Get one row by `entityKey`. |
| `PATCH /rows/{key}` | Partial update — only the fields you send are merged onto the stored payload. `404` if the row doesn't exist. |
| `DELETE /rows/{key}` | Delete one row. |
| `PATCH /rows/bulk` | Patch up to 1,000 rows by key, in one transaction with partial success. |
| `PATCH /rows/by-filter` | Patch every row matching a filter — up to 5,000 rows per call (`413` above that). |
| `DELETE /rows/bulk` | Delete up to 1,000 rows by key list. |
| `DELETE /rows/by-filter` | Delete every row matching a filter — up to 5,000 rows per call. |
| `GET /rows/schema` | The table's field definitions and total row count. |
Example: update room status
Take a rooms table with one row per room, keyed by room name (R-101, R-102 and so on). Its fields include `room_name` (Required, Unique), `status` (Required: Available, In use, Cleaning or Out of service), `room_type`, `floor` and `updated_at`. A facilities-management export can send every room in one bulk call:
curl -X POST https://app.spatialmap.io/api/v1/public/tables/rows/bulk \
-H "Authorization: Bearer $SPATIALMAP_TABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"entityKey": "R-101",
"payload": {
"room_name": "R-101",
"room_type": "Meeting room",
"status": "In use",
"floor": 1,
"updated_at": "2026-10-11 14:05:00"
}
},
{
"entityKey": "R-102",
"payload": {
"room_name": "R-102",
"room_type": "Meeting room",
"status": "Cleaning"
}
}
]
}'{
"upserted": 1,
"errors": [
{ "index": "1", "error": "unique constraint violated: room_name = R-101 (already used by row \"R-102\")" }
]
}The response above is what you'd get if the second row had reused `"room_name": "R-101"`: the first row is saved, the second is reported by its position in the list, and nothing else is lost. On the map, a Room status link using the example style rules repaints R-101 amber (Room in use) and R-102 purple (Room cleaning); open maps pick the change up within about 30 seconds.
curl -X PATCH https://app.spatialmap.io/api/v1/public/tables/rows/R-102 \
-H "Authorization: Bearer $SPATIALMAP_TABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "Available"}'Rows that should appear as points — staff badges, equipment tags — can carry an optional `"position": [longitude, latitude]` next to `payload`, or keep `lng` / `lat` inside the payload, for example in a Staff or Equipment table.
Filter trees
`/rows/query` and the two `by-filter` endpoints take a filter tree. A condition is `{"kind": "condition", "field", "op", "value"}`; a group is `{"kind": "group", "combine": "and" | "or", "conditions": [...]}`. Operators include `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between`, `contains`, `startsWith`, `endsWith`, `regex`, `in`, `notIn`, `exists`, `missing`, `before`, `after`, `lastNDays` and `nextNDays`.
curl -X POST https://app.spatialmap.io/api/v1/public/tables/rows/query \
-H "Authorization: Bearer $SPATIALMAP_TABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"kind": "group",
"combine": "and",
"conditions": [
{ "kind": "condition", "field": "status", "op": "eq", "value": "In use" },
{ "kind": "condition", "field": "room_type", "op": "in", "value": ["Meeting room", "Office"] }
]
},
"page": 1,
"pageSize": 50,
"includeCount": true
}'Validation and errors
| Status | When |
|---|---|
| `400` | The body isn't valid JSON, `payload` isn't an object, a Required field is missing or null, a Unique value is already used by another row, or a bulk call has more than 1,000 rows. |
| `401` | Missing, revoked or unknown key. |
| `404` | `GET`, `PATCH` or `DELETE` on a key that doesn't exist. |
| `413` | A `by-filter` call would touch more than 5,000 rows. |
| `429` | "rate limit exceeded for this token" — see below. |
Field types aren't enforced by the API: a value is stored as you send it. Send numbers as JSON numbers so number filters and charts treat them as numbers.
Rate limit
Each key may make 60 requests a minute, with short bursts of up to 30 allowed. Over that, requests get `429` until the budget refills. A bulk call counts as one request, so batch your writes: one `POST /rows/bulk` with 1,000 rows instead of 1,000 single calls. Bulk request bodies can be up to 32 MB.
More in Data
- Data · IntegrationsConnect data with IntegrationsBring data into Spatial Map three ways: poll a REST API, upload a CSV, Excel, JSON or GeoJSON file, or create a table your systems write to.
- Data · FilesManage floor plan and 3D model filesSee every floor plan, image plane and 3D model your workspace stores, which facilities use them, and safely free up storage taken by unused files.