Spaces.
The groups of your project's devices. Each space runs a branch — a Developer space on dev beside the Default on main — and a private one keeps records, files and settings of its own: a customer of yours, kept apart from the rest. Make and change them here, move devices between them, and act inside one with the Arda-Space header.
About spaces
Every device of a project is in one space, or in none. A space decides two things about the devices in it: which branch they run, and — when it is private — whose data they read.
- A branch each. A space names one of the project's branches; its devices run that branch's app, and changing the branch moves every device in the space onto it. A Developer space on
devbeside the Default onmainis the whole of a test bench. - Private: data of its own. A private space's records, files and setting values are its own — kept apart from the project's and from every other space's, as two projects are. Its datasets and settings are the app's, made in it when it turns private and kept in step after every push; each setting starts from the project's value. This is the space for one of your customers: a till company's shop, with its own products and its own sales. The portal shows a private space's data only as a summary; your system reads and writes it here, with
Arda-Space. - Shared: the project's data. A space that is not private reads the project's own records and files, like the Default.
- Its own limits.
max_devicescaps how many devices it may hold; a private one may havestorage_mbfor its own data. Both sit inside the account's own limits; leave them out for none. - The plan decides how many.
limits.max_spacesinGET /v1/authis how many the account may make beyond each project's Default, across all its projects. A space is not a project to the plan's project limit, and a private one's datasets do not count against the dataset limit. - Managed with the project's key, area
spaces, all four verbs. A key made before 2026-10-27 has nospacesentry in its scopes until it is edited in the portal. - Licences. On an app bought from the marketplace, the devices of every space share the project's licences — first activated, first licensed.
Default and Unassigned
Two names are reserved, and you can use them wherever this API takes a space:
default— every project's Default space. A device lands in it unless it is told another. It keeps its name and slug, it is never private (it is the project's data), and it is never removed (409 SPACE_DEFAULT). Its branch and device limit change like any space's.unassigned— no space at all: a device activated without one, or left by a space that was removed. It runs no app — its screen says it is waiting for a space — and nothing is deployed to it until it is moved into one. Nothing on it is deleted, and a new player version reaches it as soon as one is out. Register a stock of devices intounassigned, and move each into its customer's space when it ships.
Acting inside a space
Send Arda-Space: <id or slug> with any other call and it acts inside that space. For a private space the call reads and writes the space's own data — its records, files and settings. For any other the data is the project's, so the header changes nothing there. On /v1/devices it narrows the list to that space and activates into it. The key's scopes still decide what the call may do; a space that is not one of this project's is 404 NO_SPACE.
- Inside a private space, its datasets are the app's. A dataset cannot be made, changed or dropped there (
409 SPACE_DEFINED): change it on the project, without the header, and every private space follows. /v1/spacesitself refuses the header: spaces are managed from their project.
Which space a call acted in — GET /v1/auth says
curl "https://api.ardaform.net/v1/auth" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Arda-Space: zz-docs-cake-co"const res = await fetch('https://api.ardaform.net/v1/auth', {
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Arda-Space': 'zz-docs-cake-co'
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.get(
"https://api.ardaform.net/v1/auth",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}", "Arda-Space": "zz-docs-cake-co"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"key": {
"name": "api npm test",
"prefix": "af_••••••••"
},
"project": {
"slug": "api-test-zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"status": "active"
},
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"private": true
}
}
The customer's own shops, written into its copy of the app's dataset
curl -X POST "https://api.ardaform.net/v1/datasets/zz-docs-branches/records" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Arda-Space: zz-docs-cake-co" \
-H "Content-Type: application/json" \
-d '[{"id":"1","name":"Soho Road"},{"id":"2","name":"Harborne"}]'const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-branches/records', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Arda-Space': 'zz-docs-cake-co',
'Content-Type': 'application/json'
},
body: JSON.stringify([{ id: '1', name: 'Soho Road' }, { id: '2', name: 'Harborne' }])
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.post(
"https://api.ardaform.net/v1/datasets/zz-docs-branches/records",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}", "Arda-Space": "zz-docs-cake-co"},
json=[{"id": "1", "name": "Soho Road"}, {"id": "2", "name": "Harborne"}],
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"dataset": "zz-docs-branches",
"written": 2,
"appended": 0,
"refused": 0,
"exceptions": [],
"cursor": 2,
"pruned": 0
}
A dataset made inside a private space is refused
curl -X POST "https://api.ardaform.net/v1/datasets" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Arda-Space: zz-docs-cake-co" \
-H "Content-Type: application/json" \
-d '{"name":"Own list","key":"zz-docs-own"}'const res = await fetch('https://api.ardaform.net/v1/datasets', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Arda-Space': 'zz-docs-cake-co',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Own list', key: 'zz-docs-own' })
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.post(
"https://api.ardaform.net/v1/datasets",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}", "Arda-Space": "zz-docs-cake-co"},
json={"name": "Own list", "key": "zz-docs-own"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "SPACE_DEFINED",
"message": "A space's datasets are its parent project's — mirrored from them. Make, change or drop the dataset on the parent (without `Arda-Space`) and every space follows; its records are the space's own."
}
}
GET/v1/spacesThe spaces of the key's project
Every space of the key's project — the Default first, then by name — with the branch it runs and how many devices it holds. unassigned counts the devices in none. Page through a long list with limit and offset; total counts them all.
Key scope spaces:r — read spaces
Query parameters
| Name | Type | |
|---|---|---|
limit | integer 1–1000 · default 100 | Page size, 1–1000, default 100. Out of range is clamped to the range, not refused; a value that is not a number is the default. |
offset | integer ≥ 0 · default 0 | How many to skip. Negative or not a number is 0. |
Answer 200 The spaces
| Field | Type | |
|---|---|---|
spaces | array of object | |
total | integer | Every space of the project, whatever the page. |
unassigned | integer | The devices in no space — nothing is deployed to them. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 409 | NOT_A_PARENT | The key's project is a private space's store, which has no spaces of its own. |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL.
curl "https://api.ardaform.net/v1/spaces" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/spaces', {
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.get(
"https://api.ardaform.net/v1/spaces",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"spaces": [
{
"id": 1,
"slug": "default",
"name": "Default",
"default": true,
"branch": "main",
"private": false,
"devices": 1,
"max_devices": null,
"storage_mb": null,
"created_at": "2026-10-27T19:53:15.181Z"
},
{
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"default": false,
"branch": "main",
"private": true,
"devices": 0,
"max_devices": 3,
"storage_mb": 50,
"created_at": "2026-10-27T20:10:20.494Z"
}
],
"total": 2,
"unassigned": 0
}
POST/v1/spacesMake a space
A new space is empty: activate devices into it, or move them in.
nameis required.slugis made from it when you leave it out;branchis the project's default branch unless you name one.private: truegives it data of its own, made at once: the app's datasets and settings are in it before the answer comes back.mirrored: falsemeans the device service did not answer in time; the definitions arrive when its first device connects.- Refused when the plan has no spaces (
403 NO_SPACES), when they are all made (403 LIMIT_REACHED, withlimitandused), for a slug the project has or a reserved one (409 SLUG_TAKEN), and for a branch the project does not have (400 NO_BRANCH).
Key scope spaces:a — add spaces
Body
| Field | Type | |
|---|---|---|
name required | string at most 100 characters | What the portal calls it. Required. |
slug | string at most 40 characters | How this API names it: small letters, digits and dashes. From the name when left out. |
branch | string at most 80 characters | The branch its devices run. The project's default branch when left out. |
private | boolean | Data of its own. False when left out: the project's data. |
max_devices | integer, nullable ≥ 1 | The most devices it may hold. |
storage_mb | integer, nullable ≥ 1 | A private space's own storage limit, in MB. |
Answer 201 Made
| Field | Type | |
|---|---|---|
space | object | One space of the key's project: a group of its devices, the branch they run, and — when private — data of their own. |
space.id | integer | The space's id. Address it by this or by slug. |
space.slug | string | How this API names it: small letters, digits and dashes, unique in the project. default is the Default space's; unassigned is reserved for the devices in none. |
space.name | string | What the portal calls it. |
space.default | boolean | The project's Default space: every project has one, a device lands in it unless told another, and it keeps its name and slug and is never removed. |
space.branch | string | The branch its devices run. Changing it moves every device in the space onto that branch. |
space.private | boolean | Its own records, files and setting values, kept apart from the project's — reached with Arda-Space. False: the project's own data. |
space.devices | integer | The devices in it. |
space.max_devices | integer, nullable | The most devices it may hold, or null for only the account's limit. |
space.storage_mb | integer, nullable | A private space's own storage limit, in MB; null when it has none or is not private. |
space.created_at | string (date-time) | When it was made. UTC. |
space.used_bytes | integer, nullable | Only on GET /v1/spaces/{space}: what a private space's own records and files take now; null on any other. |
mirrored | boolean, nullable | A private space: the app's datasets and settings are already in it. Null on any other. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 400 | NO_BRANCH | The project has no branch of that name — a space runs one of the project's branches. |
| 403 | NO_SPACES | The account's plan has no spaces beyond each project's Default (max_spaces 0). |
| 403 | LIMIT_REACHED | The package's ceiling on datasets (max_datasets), webhooks (webhook_endpoints), spaces beyond each project's Default (max_spaces) or devices (max_devices), each across the whole account, is reached. |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 409 | NOT_A_PARENT | The key's project is a private space's store, which has no spaces of its own. |
| 409 | SLUG_TAKEN | Another space of this project has that slug — or it is one of the two reserved: default (the Default space) and unassigned (a device in none). |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL BAD_JSON TOO_LARGE.
curl -X POST "https://api.ardaform.net/v1/spaces" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Cake & Co, Soho Road","slug":"zz-docs-cake-co","private":true,"max_devices":3,"storage_mb":50}'const res = await fetch('https://api.ardaform.net/v1/spaces', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Cake & Co, Soho Road',
slug: 'zz-docs-cake-co',
private: true,
max_devices: 3,
storage_mb: 50
})
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.post(
"https://api.ardaform.net/v1/spaces",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={
"name": "Cake & Co, Soho Road",
"slug": "zz-docs-cake-co",
"private": True,
"max_devices": 3,
"storage_mb": 50,
},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"default": false,
"branch": "main",
"private": true,
"devices": 0,
"max_devices": 3,
"storage_mb": 50,
"created_at": "2026-10-27T20:10:20.494Z"
},
"mirrored": true
}
A slug another space has
curl -X POST "https://api.ardaform.net/v1/spaces" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Cake & Co again","slug":"zz-docs-cake-co"}'const res = await fetch('https://api.ardaform.net/v1/spaces', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Cake & Co again', slug: 'zz-docs-cake-co' })
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.post(
"https://api.ardaform.net/v1/spaces",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={"name": "Cake & Co again", "slug": "zz-docs-cake-co"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "SLUG_TAKEN",
"message": "A space of this project already has the slug 'zz-docs-cake-co'.",
"slug": "zz-docs-cake-co"
}
}
GET/v1/spaces/{space}One space
One space, by its id or slug — default is the Default — with used_bytes: what a private space's own records and files take now. Null on any other.
Key scope spaces:r — read spaces
Path parameters
| Name | Type | |
|---|---|---|
space | string at most 100 characters | The space's id or its slug — default is the Default space. Another project's space is 404 NO_SPACE, the same as none. |
Answer 200 The space
| Field | Type | |
|---|---|---|
space | object | One space of the key's project: a group of its devices, the branch they run, and — when private — data of their own. |
space.id | integer | The space's id. Address it by this or by slug. |
space.slug | string | How this API names it: small letters, digits and dashes, unique in the project. default is the Default space's; unassigned is reserved for the devices in none. |
space.name | string | What the portal calls it. |
space.default | boolean | The project's Default space: every project has one, a device lands in it unless told another, and it keeps its name and slug and is never removed. |
space.branch | string | The branch its devices run. Changing it moves every device in the space onto that branch. |
space.private | boolean | Its own records, files and setting values, kept apart from the project's — reached with Arda-Space. False: the project's own data. |
space.devices | integer | The devices in it. |
space.max_devices | integer, nullable | The most devices it may hold, or null for only the account's limit. |
space.storage_mb | integer, nullable | A private space's own storage limit, in MB; null when it has none or is not private. |
space.created_at | string (date-time) | When it was made. UTC. |
space.used_bytes | integer, nullable | Only on GET /v1/spaces/{space}: what a private space's own records and files take now; null on any other. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 404 | NO_SPACE | No space with that id or slug in the key's project — in /v1/spaces/{space}, a space field or parameter, or the Arda-Space header. unassigned is no space (the devices in none). Another project's space answers the same, so nothing can be probed for. |
| 409 | NOT_A_PARENT | The key's project is a private space's store, which has no spaces of its own. |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL.
curl "https://api.ardaform.net/v1/spaces/zz-docs-cake-co" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/spaces/zz-docs-cake-co', {
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.get(
"https://api.ardaform.net/v1/spaces/zz-docs-cake-co",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"default": false,
"branch": "main",
"private": true,
"devices": 0,
"max_devices": 3,
"storage_mb": 50,
"created_at": "2026-10-27T20:10:20.494Z",
"used_bytes": 0
}
}
An id or slug that is not this project's
curl "https://api.ardaform.net/v1/spaces/cust-9999" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/spaces/cust-9999', {
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.get(
"https://api.ardaform.net/v1/spaces/cust-9999",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "NO_SPACE",
"message": "No space 'cust-9999' in this key's project.",
"space": "cust-9999"
}
}
PATCH/v1/spaces/{space}Change a space
Send any of name, slug, branch, private, max_devices, storage_mb; null clears a limit.
- A new
branchmoves every device in the space onto it — each takes the new app as its branch's apply policy says. private: truegives the space data of its own; its devices drop the project's data and walk the space's.private: falsedeletes that data and its devices go back to the project's — it needs?confirm=1.- A lower
max_devicesstops new devices joining and keeps the ones it holds.
Key scope spaces:w — write spaces
Path parameters
| Name | Type | |
|---|---|---|
space | string at most 100 characters | The space's id or its slug — default is the Default space. Another project's space is 404 NO_SPACE, the same as none. |
Query parameters
| Name | Type | |
|---|---|---|
confirm | string — 1 | Must be 1 to turn private off: the space's own data is deleted. |
Body
| Field | Type | |
|---|---|---|
name | string at most 100 characters | What the portal calls it. |
slug | string at most 40 characters | How this API names it. |
branch | string at most 80 characters | The branch its devices run. |
private | boolean | Data of its own. Turning it off deletes that data (?confirm=1). |
max_devices | integer, nullable ≥ 1 | The most devices it may hold, or null for none of its own. |
storage_mb | integer, nullable ≥ 1 | A private space's storage limit in MB, or null. |
Answer 200 Changed
| Field | Type | |
|---|---|---|
space | object | One space of the key's project: a group of its devices, the branch they run, and — when private — data of their own. |
space.id | integer | The space's id. Address it by this or by slug. |
space.slug | string | How this API names it: small letters, digits and dashes, unique in the project. default is the Default space's; unassigned is reserved for the devices in none. |
space.name | string | What the portal calls it. |
space.default | boolean | The project's Default space: every project has one, a device lands in it unless told another, and it keeps its name and slug and is never removed. |
space.branch | string | The branch its devices run. Changing it moves every device in the space onto that branch. |
space.private | boolean | Its own records, files and setting values, kept apart from the project's — reached with Arda-Space. False: the project's own data. |
space.devices | integer | The devices in it. |
space.max_devices | integer, nullable | The most devices it may hold, or null for only the account's limit. |
space.storage_mb | integer, nullable | A private space's own storage limit, in MB; null when it has none or is not private. |
space.created_at | string (date-time) | When it was made. UTC. |
space.used_bytes | integer, nullable | Only on GET /v1/spaces/{space}: what a private space's own records and files take now; null on any other. |
moved | integer | The devices moved onto its new branch or data. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 400 | NO_BRANCH | The project has no branch of that name — a space runs one of the project's branches. |
| 400 | CONFIRM_REQUIRED | Emptying a whole dataset, removing a space (its devices to Unassigned, a private one's own data deleted), or turning a space's private off (its own data deleted) needs ?confirm=1. |
| 400 | NOTHING_TO_DO | A PATCH sent no field it can change (a dataset with none of its fields; a device policy with no key). |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 404 | NO_SPACE | No space with that id or slug in the key's project — in /v1/spaces/{space}, a space field or parameter, or the Arda-Space header. unassigned is no space (the devices in none). Another project's space answers the same, so nothing can be probed for. |
| 409 | NOT_A_PARENT | The key's project is a private space's store, which has no spaces of its own. |
| 409 | SLUG_TAKEN | Another space of this project has that slug — or it is one of the two reserved: default (the Default space) and unassigned (a device in none). |
| 409 | SPACE_DEFAULT | The Default space keeps its name and slug, uses the project's own data, and is never removed — every project has it. Its branch and max_devices change like any space's. |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL BAD_JSON TOO_LARGE.
curl -X PATCH "https://api.ardaform.net/v1/spaces/zz-docs-cake-co" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"max_devices":5,"storage_mb":null}'const res = await fetch('https://api.ardaform.net/v1/spaces/zz-docs-cake-co', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ max_devices: 5, storage_mb: null })
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.patch(
"https://api.ardaform.net/v1/spaces/zz-docs-cake-co",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={"max_devices": 5, "storage_mb": None},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"default": false,
"branch": "main",
"private": true,
"devices": 0,
"max_devices": 5,
"storage_mb": null,
"created_at": "2026-10-27T20:10:20.494Z"
},
"moved": 0
}
The Default keeps its name
curl -X PATCH "https://api.ardaform.net/v1/spaces/default" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Main"}'const res = await fetch('https://api.ardaform.net/v1/spaces/default', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Main' })
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.patch(
"https://api.ardaform.net/v1/spaces/default",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={"name": "Main"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "SPACE_DEFAULT",
"message": "The Default space keeps its name and slug, and uses the project's own data."
}
}
DELETE/v1/spaces/{space}Remove a space, its devices to Unassigned
Removes the space. Its devices go to Unassigned — still activated, running no app until they are moved — and a private space's own data is deleted. An install code made for it registers into Unassigned from then on. The Default is never removed. It cannot be undone, so it needs ?confirm=1; moved says how many devices went to Unassigned.
Key scope spaces:d — delete spaces
Path parameters
| Name | Type | |
|---|---|---|
space | string at most 100 characters | The space's id or its slug — default is the Default space. Another project's space is 404 NO_SPACE, the same as none. |
Query parameters
| Name | Type | |
|---|---|---|
confirm required | string — 1 | Must be 1: removing a space cannot be undone. |
Answer 200 Removed
| Field | Type | |
|---|---|---|
deleted | boolean | Always true. |
space | object | One space of the key's project: a group of its devices, the branch they run, and — when private — data of their own. |
space.id | integer | The space's id. Address it by this or by slug. |
space.slug | string | How this API names it: small letters, digits and dashes, unique in the project. default is the Default space's; unassigned is reserved for the devices in none. |
space.name | string | What the portal calls it. |
space.default | boolean | The project's Default space: every project has one, a device lands in it unless told another, and it keeps its name and slug and is never removed. |
space.branch | string | The branch its devices run. Changing it moves every device in the space onto that branch. |
space.private | boolean | Its own records, files and setting values, kept apart from the project's — reached with Arda-Space. False: the project's own data. |
space.devices | integer | The devices in it. |
space.max_devices | integer, nullable | The most devices it may hold, or null for only the account's limit. |
space.storage_mb | integer, nullable | A private space's own storage limit, in MB; null when it has none or is not private. |
space.created_at | string (date-time) | When it was made. UTC. |
space.used_bytes | integer, nullable | Only on GET /v1/spaces/{space}: what a private space's own records and files take now; null on any other. |
moved | integer | The devices moved to Unassigned. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 400 | CONFIRM_REQUIRED | Emptying a whole dataset, removing a space (its devices to Unassigned, a private one's own data deleted), or turning a space's private off (its own data deleted) needs ?confirm=1. |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 404 | NO_SPACE | No space with that id or slug in the key's project — in /v1/spaces/{space}, a space field or parameter, or the Arda-Space header. unassigned is no space (the devices in none). Another project's space answers the same, so nothing can be probed for. |
| 409 | NOT_A_PARENT | The key's project is a private space's store, which has no spaces of its own. |
| 409 | SPACE_DEFAULT | The Default space keeps its name and slug, uses the project's own data, and is never removed — every project has it. Its branch and max_devices change like any space's. |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL.
curl -X DELETE "https://api.ardaform.net/v1/spaces/zz-docs-cake-co?confirm=1" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/spaces/zz-docs-cake-co?confirm=1', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.delete(
"https://api.ardaform.net/v1/spaces/zz-docs-cake-co?confirm=1",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"deleted": true,
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road",
"default": false,
"branch": "main",
"private": true,
"devices": 0,
"max_devices": 5,
"storage_mb": null,
"created_at": "2026-10-27T20:10:20.494Z"
},
"moved": 0
}
Without ?confirm=1
curl -X DELETE "https://api.ardaform.net/v1/spaces/zz-docs-cake-co" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/spaces/zz-docs-cake-co', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.delete(
"https://api.ardaform.net/v1/spaces/zz-docs-cake-co",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "CONFIRM_REQUIRED",
"message": "Removing space 'zz-docs-cake-co' moves its devices to Unassigned and deletes its own data; add ?confirm=1 to do it."
}
}
Devices per space
Every device on /v1/devices says its space — null when it is Unassigned. GET /v1/devices?space=<id or slug> lists one space's devices, ?space=default the Default's and ?space=unassigned the ones in none; an Arda-Space header narrows the list the same way. PATCH /v1/devices/{uid} with {"space": …} moves one.
The devices in no space
curl "https://api.ardaform.net/v1/devices?space=unassigned" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/devices?space=unassigned', {
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`
}
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.get(
"https://api.ardaform.net/v1/devices?space=unassigned",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"devices": []
}
A device moved into a customer's private space
curl -X PATCH "https://api.ardaform.net/v1/devices/aa11aa11aa11aa11aa11a9e5" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"space":"zz-docs-cake-co"}'const res = await fetch('https://api.ardaform.net/v1/devices/aa11aa11aa11aa11aa11a9e5', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ space: 'zz-docs-cake-co' })
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.patch(
"https://api.ardaform.net/v1/devices/aa11aa11aa11aa11aa11a9e5",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={"space": "zz-docs-cake-co"},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"device": {
"uid": "aa11aa11aa11aa11aa11a9e5",
"name": "zz-docs-tablet",
"os": "android",
"arch": "arm64",
"model": "Samsung SM-X200",
"player": "v1.2.4",
"branch": "main",
"partition": null,
"location": "Counter 1",
"enrolled": false,
"space": {
"id": 850,
"slug": "zz-docs-cake-co",
"name": "Cake & Co, Soho Road"
},
"presence": "never",
"online": false,
"last_seen_at": null,
"policy": {
"exitAllowed": false,
"kiosk": true,
"orientation": "landscape"
}
},
"moved": true,
"applies": "when the device next connects"
}
POST/v1/devices/activateActivate a device by the code on its screen
What typing the code into the portal's Add device does, from your own system: the device showing code on its screen joins the key's project and starts the app its space runs. space names the space — an id or slug, default, or unassigned to register it into none; left out, the space Arda-Space names, else the Default. name, partition and location are what the portal would ask for. The area is devices, verb w.
- A fresh device shows its code on its screen until it is activated; the example's code is shown by no device, so the answer is the refusal.
- Refused when the account's devices are all used (
403 LIMIT_REACHED), for a space that is not there (404 NO_SPACE), when no device is showing the code (404 NO_PENDING_DEVICE), when the space already holds itsmax_devices(409 SPACE_FULL), and for anything else the device service says (409 NOT_ACTIVATED, its words inreason).
Key scope devices:w — write devices
Body
| Field | Type | |
|---|---|---|
code required | string | The activation code the device shows. Required. |
name | string at most 100 characters | What to call it. |
space | string, nullable | The space it joins: id, slug, default, or unassigned / null for none. |
partition | string, nullable at most 80 characters | Its data partition. |
location | string, nullable at most 120 characters | Where it is. |
Answer 201 Activated
| Field | Type | |
|---|---|---|
device | object | One device of the key's project, as ArdaForm knows it — its identity, whether it is connected, and the policy ArdaForm holds for it. On one device, android is what the device last reported about itself. |
device.uid | string | The device's id — 24 hex characters, the one on its page in the portal. How you address it. |
device.name | string | What it was named in the portal. |
device.os | string, nullable | The player's operating system as it reports it: linux, android, … |
device.arch | string, nullable | The processor family: arm64, amd64, … |
device.model | string, nullable | The hardware, as the device reports it. |
device.player | string, nullable | The ArdaPlayer version it last reported. |
device.branch | string, nullable | The code branch it runs — its space's. Null when it is Unassigned: it runs no app. |
device.partition | string, nullable | Its data partition, or null when it has none. |
device.location | string, nullable | Where it is, as typed in the portal. Null when nothing was typed. |
device.enrolled | boolean | False once it was removed (wiped), or never activated — its record stays. |
device.space | object, nullable | The space it is in: id, slug, name. Null when it is in none — Unassigned. |
device.presence | string — online or offline or never, nullable | The one word every portal screen uses: online, offline, or never (never connected). |
device.online | boolean | Connected to ArdaForm right now. |
device.last_seen_at | string (date-time), nullable | When it was last heard from. Null when never. UTC. |
device.policy | object | The switches — and the screen's orientation — ArdaForm holds for this device and re-states to it on every check-in — only the keys that were ever set, {} when none. A key that is absent means the device keeps what it holds. |
device.android | object, nullable | Only on GET /v1/devices/{uid}, and only on an Android device: what the device last REPORTED about itself (its sysinfo.android) — kiosk is the screen's live lock state, exitAllowed what it applies now, deviceOwner whether kiosk can work at all, with release, model, appVersion and battery. It differs from policy until the device has checked in. Null on a Linux device; absent in the list. |
Errors
| Status | Word | When |
|---|---|---|
| 400 | BAD_REQUEST | The body is missing a required field, has one of the wrong type or length, or (on PUT /v1/content/…) arrived shorter or longer than its Content-Length. The message says which. |
| 403 | LIMIT_REACHED | The package's ceiling on datasets (max_datasets), webhooks (webhook_endpoints), spaces beyond each project's Default (max_spaces) or devices (max_devices), each across the whole account, is reached. |
| 403 | OUT_OF_SCOPE | The key's scopes do not allow this area and verb (or this dataset, for records). A key edited before an area existed has no entry for it and is refused there until it is edited again in the portal. |
| 404 | NO_SPACE | No space with that id or slug in the key's project — in /v1/spaces/{space}, a space field or parameter, or the Arda-Space header. unassigned is no space (the devices in none). Another project's space answers the same, so nothing can be probed for. |
| 404 | NO_PENDING_DEVICE | No device is showing that activation code right now: check the code on its screen, and that the device is switched on and online. |
| 409 | SPACE_FULL | The space already holds its max_devices. |
| 409 | NOT_ACTIVATED | The device service refused the activation — a code tried too often, a device already registered. reason says which. |
| 502 | CONNECT_UNAVAILABLE | The device service did not answer — a webhook Test, an activation, or the wipes a space removal needs. Nothing was done; try again in a moment. |
And, like every keyed call: NO_KEY BAD_KEY KEY_REVOKED KEY_EXPIRED ACCOUNT_INACTIVE NO_API_ACCESS RATE_LIMITED INTERNAL BAD_JSON TOO_LARGE.
A code no device is showing
curl -X POST "https://api.ardaform.net/v1/devices/activate" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"KX7P-2M4Q","name":"Till 1","location":"Front counter","space":"zz-docs-cake-co"}'const res = await fetch('https://api.ardaform.net/v1/devices/activate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: 'KX7P-2M4Q',
name: 'Till 1',
location: 'Front counter',
space: 'zz-docs-cake-co'
})
})
const answer = await res.json()
if (!res.ok) throw new Error(`${res.status} ${answer.error.code}: ${answer.error.message}`)
console.log(answer)import os
import requests
r = requests.post(
"https://api.ardaform.net/v1/devices/activate",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json={
"code": "KX7P-2M4Q",
"name": "Till 1",
"location": "Front counter",
"space": "zz-docs-cake-co",
},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"error": {
"code": "NO_PENDING_DEVICE",
"message": "No device is showing that code right now — check the code on its screen, and that the device is online."
}
}
The Space shape
Space
One space of the key's project: a group of its devices, the branch they run, and — when private — data of their own.
| Field | Type | |
|---|---|---|
id | integer | The space's id. Address it by this or by slug. |
slug | string | How this API names it: small letters, digits and dashes, unique in the project. default is the Default space's; unassigned is reserved for the devices in none. |
name | string | What the portal calls it. |
default | boolean | The project's Default space: every project has one, a device lands in it unless told another, and it keeps its name and slug and is never removed. |
branch | string | The branch its devices run. Changing it moves every device in the space onto that branch. |
private | boolean | Its own records, files and setting values, kept apart from the project's — reached with Arda-Space. False: the project's own data. |
devices | integer | The devices in it. |
max_devices | integer, nullable | The most devices it may hold, or null for only the account's limit. |
storage_mb | integer, nullable | A private space's own storage limit, in MB; null when it has none or is not private. |
created_at | string (date-time) | When it was made. UTC. |
used_bytes | integer, nullable | Only on GET /v1/spaces/{space}: what a private space's own records and files take now; null on any other. |