First ten minutes.
From a new key to a working sync, in seven calls. Every answer below is what the live API sent back when the call was run.
1 · A key
In the portal, open the project, then API keys → New key. Tick what the key may do, area by area; for a first session, everything on Datasets and Records is enough. The whole key is shown once — copy it then.
# every example on these pages reads the key from here
export ARDA_KEY=af_…your key…
2 · Confirm it
The answer names the key, what it may do (scopes), its project, and the account's limits. Read limits once when your program starts and you will never discover a ceiling by hitting it.
curl "https://api.ardaform.net/v1/auth" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/auth', {
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/auth",
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){
"key": {
"name": "api npm test",
"prefix": "af_••••••••",
"scope": "write",
"branch": null,
"scopes": {
"code": {
"r": "*",
"w": "*",
"a": "*",
"d": "*"
},
"datasets": {
"r": "*",
"w": "*",
"a": "*",
"d": "*"
},
"records": {
"r": "*",
"w": "*",
"a": "*",
"d": "*"
},
"files": {
"r": "*",
"w": "*",
"a": "*",
"d": "*"
}
},
"expires_on": null,
"created_at": "2026-08-21T08:49:42.652Z"
},
"project": {
"slug": "api-test",
"name": "API test",
"status": "active"
},
"space": null,
"account": {
"name": "ArdaForm Test"
},
"limits": {
"max_projects": 3,
"max_devices": 10,
"max_users": 1,
"max_invited": 0,
"max_datasets": 20,
"max_spaces": 3,
"storage_mb": 50,
"console": false,
"marketplace": true,
"marketplace_fee": 20,
"gui": true,
"detect_priority": "normal",
"support": "email",
"webhooks": true,
"webhook_endpoints": 2,
"webhook_per_min": 60,
"api_access": true,
"api_keys": 2,
"api_per_min": 600,
"api_burst": 1200,
"max_records_per_call": 10000,
"max_bytes_per_call": 10485760
}
}
A key that has no webhooks, project or settings entry — like this one, made in August — is refused on those areas until it is edited in the portal. Why.
3 · A dataset
Usually a dataset comes from your app's manifest.json when a commit is packed. Data that only your own systems use can have one made here. This one is split by a field called branch, so a device reads only its branch's records.
curl -X POST "https://api.ardaform.net/v1/datasets" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Docs products","key":"zz-docs-products","description":"Prices, per branch","partition_field":"branch"}'const res = await fetch('https://api.ardaform.net/v1/datasets', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Docs products',
key: 'zz-docs-products',
description: 'Prices, per branch',
partition_field: 'branch'
})
})
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']}"},
json={
"name": "Docs products",
"key": "zz-docs-products",
"description": "Prices, per branch",
"partition_field": "branch",
},
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"dataset": {
"key": "zz-docs-products",
"name": "Docs products",
"description": "Prices, per branch",
"access": "rw",
"partition_field": "branch",
"retention_mode": "never",
"retention_value": null,
"storage_limit_bytes": null,
"cursor": 0,
"records": 0,
"bytes": 0,
"updated_at": null,
"created_at": "2026-10-27T20:10:00.663Z"
}
}
4 · Write records
A record is any JSON object. Give it an id and sending it again updates it. PUT replaces the whole dataset with what you send — the right call for a catalogue published from somewhere else. A record may sit in several branches at once: its branch is a list.
curl -X PUT "https://api.ardaform.net/v1/datasets/zz-docs-products/records" \
-H "Authorization: Bearer $ARDA_KEY" \
-H "Content-Type: application/json" \
-d '[{"id":"A1","name":"Sourdough","price":3.2,"branch":["oldbury","moseley"]},{"id":"A2","name":"Rye","price":2.9,"branch":"oldbury"}]'const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-products/records', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.ARDA_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify([
{
id: 'A1',
name: 'Sourdough',
price: 3.2,
branch: ['oldbury', 'moseley']
},
{ id: 'A2', name: 'Rye', price: 2.9, branch: 'oldbury' }
])
})
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.put(
"https://api.ardaform.net/v1/datasets/zz-docs-products/records",
headers={"Authorization": f"Bearer {os.environ['ARDA_KEY']}"},
json=[
{
"id": "A1",
"name": "Sourdough",
"price": 3.2,
"branch": ["oldbury", "moseley"],
},
{"id": "A2", "name": "Rye", "price": 2.9, "branch": "oldbury"},
],
)
answer = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code} {answer['error']['code']}: {answer['error']['message']}")
print(answer){
"dataset": "zz-docs-products",
"replaced": 2,
"pruned": 0,
"cursor": 3,
"refused": 0,
"exceptions": []
}
5 · Read them
Filter by the partition field's own name — ?branch=oldbury. Sourdough is in Oldbury and Moseley, so it is here; has_more says there is another page.
curl "https://api.ardaform.net/v1/datasets/zz-docs-products/records?branch=oldbury&limit=2" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-products/records?branch=oldbury&limit=2', {
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/datasets/zz-docs-products/records?branch=oldbury&limit=2",
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){
"dataset": "zz-docs-products",
"total": 2,
"limit": 2,
"offset": 0,
"has_more": false,
"records": [
{
"key": "A1",
"partitions": [
"moseley",
"oldbury"
],
"created_at": "2026-10-27T20:10:01.364Z",
"updated_at": "2026-10-27T20:10:01.364Z",
"data": {
"id": "A1",
"name": "Sourdough",
"price": 3.2,
"branch": [
"oldbury",
"moseley"
]
}
},
{
"key": "A2",
"partitions": [
"oldbury"
],
"created_at": "2026-10-27T20:10:01.364Z",
"updated_at": "2026-10-27T20:10:01.462Z",
"data": {
"id": "A2",
"name": "Rye",
"price": 3.05,
"branch": "oldbury"
}
}
]
}
6 · Sync
To keep a copy in step, ask for what changed since a cursor. since=0 is everything; store the cursor you get and send it next time.
curl "https://api.ardaform.net/v1/datasets/zz-docs-products/changes?since=0" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-products/changes?since=0', {
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/datasets/zz-docs-products/changes?since=0",
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){
"dataset": "zz-docs-products",
"since": 0,
"cursor": 6,
"has_more": false,
"purged": true,
"changed": [
{
"key": "A1",
"partitions": [
"moseley",
"oldbury"
],
"created_at": "2026-10-27T20:10:01.364Z",
"updated_at": "2026-10-27T20:10:01.364Z",
"data": {
"id": "A1",
"name": "Sourdough",
"price": 3.2,
"branch": [
"oldbury",
"moseley"
]
}
},
{
"key": "A2",
"partitions": [
"oldbury"
],
"created_at": "2026-10-27T20:10:01.364Z",
"updated_at": "2026-10-27T20:10:01.462Z",
"data": {
"id": "A2",
"name": "Rye",
"price": 3.05,
"branch": "oldbury"
}
},
{
"key": "A3",
"partitions": [
"moseley"
],
"created_at": "2026-10-27T20:10:01.462Z",
"updated_at": "2026-10-27T20:10:01.462Z",
"data": {
"id": "A3",
"name": "Bagel",
"price": 1.2,
"branch": "moseley"
}
},
{
"key": "A4",
"partitions": [
"moseley"
],
"created_at": "2026-10-27T20:10:01.541Z",
"updated_at": "2026-10-27T20:10:01.541Z",
"data": {
"id": "A4",
"name": "Scone",
"price": 1.5,
"branch": "moseley"
}
}
],
"deleted": []
}
After one record was deleted and another updated, the next call with that cursor answered just those two — the update in changed, the deletion in deleted:
curl "https://api.ardaform.net/v1/datasets/zz-docs-products/changes?since=6" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-products/changes?since=6', {
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/datasets/zz-docs-products/changes?since=6",
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){
"dataset": "zz-docs-products",
"since": 6,
"cursor": 8,
"has_more": false,
"purged": false,
"changed": [
{
"key": "A1",
"partitions": [
"moseley",
"oldbury"
],
"created_at": "2026-10-27T20:10:01.364Z",
"updated_at": "2026-10-27T20:10:03.501Z",
"data": {
"id": "A1",
"name": "Sourdough",
"price": 3.4,
"branch": [
"oldbury",
"moseley"
]
}
}
],
"deleted": [
"A3"
]
}
7 · Clean up
Deleting a dataset takes every record with it, and says how many.
curl -X DELETE "https://api.ardaform.net/v1/datasets/zz-docs-products" \
-H "Authorization: Bearer $ARDA_KEY"const res = await fetch('https://api.ardaform.net/v1/datasets/zz-docs-products', {
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/datasets/zz-docs-products",
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": {
"dataset": "zz-docs-products",
"records": 0
}
}
Next
- The sync in full: windows, purges, partitions, and where the first cursor comes from.
- Narrow the key to what your program needs — a till key that appends to one dataset and reads another.
- The error words to handle.