Versioning.

The version is in the path, and there is no default. v1 changes only in ways that cannot break a client written against it.

Working — v1 at api.ardaform.net

In the path

  • Every endpoint starts /v1/. There is no unversioned alias and no "latest": a client that did not say which API it was written against would break silently the day the next one ships. Anything else is 404 NO_VERSION.
  • When a change would break a client, it becomes v2, beside v1. v1 keeps answering, from the same service. There is no separate test API either: the version in the path is the whole story.

What changes inside v1

May happen in v1Would be v2
a new endpoint, a new area of scopesan endpoint removed or moved
a new field in an answer (ignore fields you do not know)a field removed, renamed or retyped
a new optional body field or query parametera new required one
a new error word for a new refusalan existing word changing meaning or status
a message reworded—
a bug fixed so the API does what these pages say—

Recent examples of the last row, from 2026-10-27: /v1/auth/anything answered as if it were /v1/auth, POST /v1 answered the index, and /changes did not honour a dataset the app's manifest closes to the API. All three now answer as documented.

A new area of scopes is refused for keys whose scopes were written before it existed — see Authentication. That is the one addition a key has to be edited for.

GET/v1Every endpoint of v1, in one short answer

Every endpoint of v1, one line each — the quickest way to see what exists. No key needed, not counted against the rate. Derived from the OpenAPI document, so the two cannot disagree.

Key scope no key needed

Answer 200 The index

FieldType
versionstringAlways v1 here.
openapistringWhere this document is.
endpointsarray of stringMETHOD /path, one per operation.

Errors

Only what every call can answer: INTERNAL.

curl "https://api.ardaform.net/v1"
Answer · 200 OKran live 2026-10-27 · test project
{
  "version": "v1",
  "openapi": "https://api.ardaform.net/v1/openapi.json",
  "endpoints": [
    "GET    /v1",
    "GET    /v1/openapi.json",
    "GET    /v1/auth",
    "GET    /v1/project",
    "GET    /v1/settings",
    "GET    /v1/settings/{key}",
    "PUT    /v1/settings/{key}",
    "GET    /v1/devices",
    "GET    /v1/devices/{uid}",
    "PATCH  /v1/devices/{uid}",
    "PATCH  /v1/devices/{uid}/policy",
    "GET    /v1/spaces",
    "POST   /v1/spaces",
    "GET    /v1/spaces/{space}",
    "PATCH  /v1/spaces/{space}",
    "DELETE /v1/spaces/{space}",
    "POST   /v1/devices/activate",
    "GET    /v1/files",
    "POST   /v1/files",
    "GET    /v1/files/{path}",
    "DELETE /v1/files/{path}",
    "GET    /v1/content/{path}",
    "PUT    /v1/content/{path}",
    "GET    /v1/datasets",
    "POST   /v1/datasets",
    "GET    /v1/datasets/{key}",
    "PATCH  /v1/datasets/{key}",
    "DELETE /v1/datasets/{key}",
    "GET    /v1/datasets/{key}/changes",
    "GET    /v1/datasets/{key}/records",
    "POST   /v1/datasets/{key}/records",
    "PUT    /v1/datasets/{key}/records",
    "DELETE /v1/datasets/{key}/records",
    "GET    /v1/datasets/{key}/records/{recordKey}",
    "DELETE /v1/datasets/{key}/records/{recordKey}",
    "GET    /v1/webhooks",
    "POST   /v1/webhooks",
    "POST   /v1/webhooks/verify",
    "GET    /v1/webhooks/{slug}",
    "PATCH  /v1/webhooks/{slug}",
    "DELETE /v1/webhooks/{slug}",
    "GET    /v1/webhooks/{slug}/deliveries",
    "GET    /v1/webhooks/{slug}/deliveries/{id}",
    "POST   /v1/webhooks/{slug}/test"
  ]
}

GET/v1/openapi.jsonThis document

The OpenAPI 3.0.3 description of v1, with no key needed. Import it into Postman (Import → Link), Insomnia, Bruno, Hoppscotch or Swagger UI from the link; it is always current. Every operation names its key scope in x-scope ({"area", "verb"}, with "by": "dataset" where a grant can list datasets) and its error words in x-error-codes.

A test walks every route the API answers and fails if one is missing from the document, if an operation's x-scope differs from the scope the code refuses with, or if the code answers an error word the document does not describe. The document passes Redocly's OpenAPI linter.

Key scope no key needed

Answer 200 OpenAPI 3.0.3

Errors

Only what every call can answer: INTERNAL.

Shortened here to its version and the list of paths

curl "https://api.ardaform.net/v1/openapi.json"
Answer · 200 OKran live 2026-10-27 · test project · a list shortened
{
  "openapi": "3.0.3",
  "info": {
    "title": "ArdaForm HTTP API",
    "version": "1.1.3"
  },
  "servers": [
    {
      "url": "https://api.ardaform.net",
      "description": "Live"
    }
  ],
  "paths": [
    "/v1",
    "/v1/openapi.json",
    "/v1/auth",
    "/v1/project",
    "/v1/settings",
    "/v1/settings/{key}",
    "/v1/devices",
    "/v1/devices/{uid}",
    "/v1/devices/{uid}/policy",
    "/v1/spaces",
    "/v1/spaces/{space}",
    "/v1/devices/activate",
    "/v1/files",
    "/v1/files/{path}",
    "/v1/content/{path}",
    "/v1/datasets",
    "/v1/datasets/{key}",
    "/v1/datasets/{key}/changes",
    "/v1/datasets/{key}/records",
    "/v1/datasets/{key}/records/{recordKey}",
    "/v1/webhooks",
    "/v1/webhooks/verify",
    "/v1/webhooks/{slug}",
    "/v1/webhooks/{slug}/deliveries",
    "/v1/webhooks/{slug}/deliveries/{id}",
    "/v1/webhooks/{slug}/test"
  ]
}

On the wire

  • HTTPS only, at api.ardaform.net. JSON in and out, UTF-8.
  • Timestamps are UTC, ISO 8601 with milliseconds and a Z: 2026-10-19T11:29:15.241Z. Dates (a key's expires_on) are YYYY-MM-DD.
  • Nothing is cacheable: every answer carries Cache-Control: no-store (a file's bytes: private, no-store), because an answer is scoped to one key.
  • No CORS. OPTIONS on any path answers 204 with no Access-Control-* headers, so a browser refuses every cross-origin call. This API is for programs that hold a secret; an app on a device reaches its data through window.Arda.
  • Addressed by name. Datasets by key, records by their own key, files by path, webhooks by slug, settings by key — never by an internal id.