Authentication.

Every call carries a project key. The key says which project it is for and what it may do there — area by area, verb by verb.

Working — v1 at api.ardaform.net

Keys

  • A key is made in the portal: project → API keys → New key. It belongs to that one project, so no endpoint takes a project id — the key already says which project, and nothing a call sends can reach another one.
  • It begins af_, so a leaked one is easy to find in a log or a repository scan.
  • It exists once, in the portal's answer when it is made. ArdaForm keeps only its SHA-256 and its first eight characters (the prefix you see in lists). Lose it and make another; nothing can show it again, and no answer ever repeats it.
  • How many keys an account may have is its package's api_keys. The rate limit is per account, so a second key does not buy a second allowance.
  • Keep it on a server. This API answers no cross-origin request, so a key in a web page does not work — and would be a key anybody can read.

As a bearer token, on every call except GET /v1 and GET /v1/openapi.json:

HTTP
Authorization: Bearer af_…
  • No header, or one not of the form Bearer af_…, is 401 NO_KEY.
  • A key-shaped token that matches no key is 401 BAD_KEY.
  • A key that was revoked is 401 KEY_REVOKED; one past its expiry day is 401 KEY_EXPIRED — two different conversations, so two words.
  • An account that is suspended or closed is 403 ACCOUNT_INACTIVE; a package without API access is 403 NO_API_ACCESS.

No key at all

curl "https://api.ardaform.net/v1/auth"
Answer · 401 Unauthorizedran live 2026-10-27 · test project
{
  "error": {
    "code": "NO_KEY",
    "message": "Send your API key as `Authorization: Bearer af_…`."
  }
}

A key that does not exist

curl "https://api.ardaform.net/v1/auth" \
  -H "Authorization: Bearer af_this-key-does-not-exist"
Answer · 401 Unauthorizedran live 2026-10-27 · test project
{
  "error": {
    "code": "BAD_KEY",
    "message": "No key matches the one sent."
  }
}

Scopes

A key carries a scopes document, set in the portal's key editor: for each area of the API, which verbs it may use. The API has no endpoint for changing a key, on purpose.

AreaCoversVerbs
datasetsthe dataset objects — /v1/datasetsr w a d
recordsthe data — …/records and …/changes. The only area that can be narrowed to named datasets.r w d (a is not used)
filesfiles and their bytes — /v1/files, /v1/contentr w d (a is not used: registering and uploading are w)
webhooks/v1/webhooks: r lists, reads the log, answers /verify; a creates; w changes one or runs a Test; d deletesr w a d
project/v1/project, read-onlyr only
settings/v1/settings: r reads, w sets a project valuer w (settings are defined by the app's manifest)
codebranches. Nothing serves code over this API; the entry is where a key's branch limit lives, reported as key.branchrecorded, not enforced
  • Each verb is "*" (all), or — for records — a list of dataset keys. An empty list is nothing.
  • w is "change what is there", a is "make new ones": a key that feeds one dataset has no business creating others. On records, w also allows PUT, which replaces the whole dataset.
  • Each endpoint on these pages names the scope it needs (and so does the OpenAPI document, as x-scope). A refusal is 403 OUT_OF_SCOPE naming the area, the action and, for records, the dataset as ref.
  • A useful shape: a till key with records: {r: ["products"], w: ["transactions"]} and nothing else — it reads the catalogue, writes its sales, and cannot empty either.
  • A key with no document at all (scopes: null) was made before scopes existed. It falls back to its scope: read may read everything, write may do everything. Editing it in the portal gives it a document.

Keys made before an area existed

A document is a list of what a person chose. An area added to the API after the document was written has no entry in it — and is refused, not granted: webhooks (added 2026-10-06), project and settings (both 2026-10-19).

Such a key answers 403 OUT_OF_SCOPE on that area until someone edits it in the portal and ticks the area. Nothing else changes for it. The key used for the examples on these pages was made in August: here it is, asking for the project.

A key written before the project area existed

curl "https://api.ardaform.net/v1/project" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 403 Forbiddenran live 2026-10-27 · test project
{
  "error": {
    "code": "OUT_OF_SCOPE",
    "message": "This key may not read project.",
    "area": "project",
    "action": "r"
  }
}

Expiry and revocation

  • key.expires_on is a date, or null for never. A key works all through its expiry day (UTC) and answers 401 KEY_EXPIRED, carrying expired_on, after it.
  • A revoked key answers 401 KEY_REVOKED. Revoking is in the portal, and it is immediate.
Answer · 401 Unauthorized not run — the shape, from the API's code
{
  "error": {
    "code": "KEY_EXPIRED",
    "message": "That key expired on 2026-10-05",
    "expired_on": "2026-10-05"
  }
}

Who this key is

GET/v1/authConfirm a key and see what it is for

Confirms a key works and says what it may do: the first call a program makes. limits is the account's allowance — package plus any overrides — and the API's own per-call ceilings; read it once at start-up.

  • key.scope is the coarse summary (write if the key may write anything at all); key.scopes is the document that decides. Read the second.
  • key.branch is the one branch the key is limited to, or null for none or several.
  • The token itself is never in the answer — only its prefix, masked here.

Key scope any valid key

Answer 200 The key, its project and the account limits

FieldType
keyobject
key.namestringWhat the key was called in the portal.
key.prefixstringThe first eight characters, af_ included. The token itself is stored only as a sha256 and cannot be shown again by anything.
key.scopestring — read or writeThe coarse summary: write if the key may write anything at all.
key.branchstring, nullableThe one branch the key is limited to, derived from its code scopes; null when limited to none or several.
key.scopesobject, nullableWhat one API key may do, per area of the API. Null on a key made before scopes existed — which means it falls back to scope, NOT that it may do nothing.
key.expires_onstring (date), nullableThe last DAY this key works, or null for never. It ends the day rather than the midnight before it. After that the answer is 401 KEY_EXPIRED.
key.created_atstring (date-time)When the key was made. UTC.
projectobject
project.slugstringThe project's slug — frozen at creation.
project.namestringFor people.
project.statusstringThe project's status, e.g. active.
spaceobject, nullableThe space this call acted in (Arda-Space), or null.
space.idintegerThe space's id.
space.slugstringIts slug.
space.namestringWhat the portal calls it.
space.privatebooleanIts data is its own — project is then its store.
accountobject
account.namestringThe account's name.
limitsobjectThe package's resolved allowance (more fields than listed — the package's own), plus the API's own per-call ceilings.

Errors

StatusWordWhen
404NO_SPACENo 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.

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/auth" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "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
  }
}

The key scopes shape

Key scopes

What one API key may do, per area of the API. Null on a key made before scopes existed — which means it falls back to scope, NOT that it may do nothing.

FieldType
codeobjectBranches. Nothing serves code over this API yet, so these are recorded rather than enforced — but they are where a key's branch scope lives, and branch is derived from them.
datasetsobjectManaging the dataset objects themselves — /v1/datasets.
recordsobjectThe data — /v1/datasets/{key}/records and /changes. THE ONE AREA THAT CAN BE NARROWED TO PARTICULAR DATASETS, which is the useful shape: a till key that appends to transactions and reads products and touches nothing else.
filesobjectAssets — /v1/files and /v1/content.
webhooksobjectWebhooks — /v1/webhooks. r lists them, reads the delivery log and answers /verify; a creates; w changes one or runs a Test; d deletes. A document written before this area existed has no entry for it and is refused here until the key is edited in the portal.
projectobjectThe project itself — /v1/project. READ ONLY: r is the one verb this area has, because a project's settings are changed in the portal and nowhere else. A document written before this area existed has no entry for it and is refused here until the key is edited in the portal.
settingsobjectThe app's settings — /v1/settings. r reads the definitions and the project values; w sets a project value the manifest lets the API write. No a or d: settings are defined by the app's manifest.json. A document written before this area existed has no entry for it and is refused here until the key is edited in the portal.
devicesobjectThe project's devices — /v1/devices. r lists them and reads one; w sets a device's policy (its switches) and activates a device by its code (POST /v1/devices/activate). No a or d: a device is removed in the portal. A document written before this area existed (2026-10-24) has no entry for it and is refused here until the key is edited in the portal.
spacesobjectSpaces — /v1/spaces: the project's groups of devices, each with the branch it runs and, when private, data of its own. r lists and reads; a makes one; w changes one (its branch moves its devices); d removes one, its devices to Unassigned. Acting INSIDE a space (Arda-Space) needs the scope of the area acted on, not this one. A document written before this area existed (2026-10-27) has no entry for it and is refused here until the key is edited in the portal.
rstringInside each area: "*" for any, or a list of dataset keys. Read.
wstringWrite — change what is already there.
astringAdd — create new ones. Separate from w because "may change the datasets it was given" and "may make more" are different powers.
dstringDelete.