Webhooks.

A webhook is an address of yours that the app on a device may send a message to with Arda.webhook.call(slug, data). ArdaForm makes the call, signs it and keeps a log; the device never holds your key and never talks to your server.

Working — v1 at api.ardaform.net

About webhooks

  • Addressed by slug, the word the app uses — Arda.webhook.call('orders', {…}). The slug never changes once made; verify cannot be one.
  • The key is write-only. Set it with "key": "…"; no answer ever contains it — only hasKey. When set, every delivery carries it as Authorization: Bearer and is signed with it.
  • A webhook with no url only records. The app's call is answered ok with status 202, nothing is sent anywhere, and the call waits in the delivery log for your systems to read: a mailbox.
  • A url must be http or https on a public host: localhost, loopback, private and link-local addresses are refused when saved, and refused again when the call is made, after the name is resolved.
  • What your server receives, and how to check it came from ArdaForm: Receiving webhooks.
  • The package decides: webhooks (on or off), webhook_endpoints (how many, across the account) and webhook_per_min (deliveries a minute). Reading always works.

GET/v1/webhooksEvery webhook on the key's project

Every webhook of the key's project, by slug.

Key scope webhooks:r — read webhooks

Answer 200 The list

FieldType
webhooksarray of Webhook

Errors

StatusWordWhen
403OUT_OF_SCOPEThe 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.
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/webhooks" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhooks": [
    {
      "slug": "zz-docs-mailbox",
      "name": "Docs mailbox",
      "url": null,
      "method": "post",
      "trigger": "app",
      "dataset": null,
      "debounceSeconds": 2,
      "hasKey": false,
      "verify": false,
      "enabled": true,
      "note": "Check-ins, read by the nightly job",
      "createdAt": "2026-10-27T20:10:10.248Z",
      "updatedAt": "2026-10-27T20:10:10.248Z"
    },
    {
      "slug": "zz-docs-receiver",
      "name": "Docs receiver",
      "url": "https://ardaform.net/health",
      "method": "post",
      "trigger": "app",
      "dataset": null,
      "debounceSeconds": 2,
      "hasKey": true,
      "verify": true,
      "enabled": true,
      "note": null,
      "createdAt": "2026-10-27T20:10:10.345Z",
      "updatedAt": "2026-10-27T20:10:10.345Z"
    }
  ]
}

POST/v1/webhooksCreate a webhook

Makes a webhook. name and slug are required. With verify: true every delivery also carries a one-time token your server can hand back to prove the call came from ArdaForm.

The receiver here points at ArdaForm's own website, which answers any request with ok — a public address for the examples.

Key scope webhooks:a — add webhooks

Body

FieldType
name requiredstring
at least 1 character · at most 120 characters
For people. Trimmed; at most 120 characters.
urlstring, nullable
at most 1024 characters
http or https, at most 1024 characters, a PUBLIC host: localhost and loopback, private and link-local addresses are 400 BAD_URL (and refused again by the plane at call time, after the name resolves). Absent, null or "" makes a webhook that only RECORDS its calls (url: null in the answer).
methodstring — post or get or putpost (the default) and put send the envelope as a JSON body; get sends it as query parameters.
triggerstring — app or datasetapp (the default): the device application calls it. dataset: the plane calls it when records of dataset change, debounceSeconds after the last change, one call per window.
datasetstring, nullableFor trigger: dataset: the key of one of this project's datasets. 400 BAD_REQUEST for a key the project has not got.
debounceSecondsinteger
0–300
For trigger: dataset: the wait after a change for more, before the one call. Default 2.
keystring, nullable
at most 255 characters
WRITE-ONLY, at most 255 characters. Sent as Authorization: Bearer and used to sign every delivery (X-Arda-Signature). Never returned — read hasKey. On PATCH, null or "" clears it.
verifybooleanMint a one-time X-Arda-Verify token per delivery, for POST /v1/webhooks/verify. Default false; no effect while there is no url. JSON true/false (or 1/0) — a string is refused.
enabledbooleanDefault true. Off, the app's calls are refused on the device and nothing is sent. JSON true/false (or 1/0).
notestring, nullable
at most 255 characters
Whatever helps the next person. At most 255 characters; null clears it.
slug requiredstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
What the app says. Lower-case letters, digits, hyphen and underscore, starting with a letter or digit, up to 80. verify is reserved.

Answer 201 Created

FieldType
webhookWebhookAn HTTP endpoint of yours that the app on a device may send a message to — Arda.webhook.call(slug, data). The plane makes the call, signs it and records every delivery; the device never holds your key and never talks to your server directly.

Errors

StatusWordWhen
400BAD_REQUESTThe 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.
400BAD_KEY_FORMATA dataset key or webhook slug is not lower-case letters, digits, - and _, starting with a letter or digit, at most 80 — or is the reserved verify (webhooks).
400BAD_URLThe url is not http or https, is over 1024 characters, does not parse, or names localhost, a loopback, private or link-local address.
403NO_WEBHOOKSThe account's package does not include webhooks. Reads still work.
403LIMIT_REACHEDThe 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.
403OUT_OF_SCOPEThe 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.
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.
409WEBHOOK_EXISTSA webhook with that slug already exists on the project.

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/webhooks" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Docs receiver","slug":"zz-docs-receiver","url":"https://ardaform.net/health","key":"whsec_docs_example_key","verify":true}'
Answer · 201 Createdran live 2026-10-27 · test project
{
  "webhook": {
    "slug": "zz-docs-receiver",
    "name": "Docs receiver",
    "url": "https://ardaform.net/health",
    "method": "post",
    "trigger": "app",
    "dataset": null,
    "debounceSeconds": 2,
    "hasKey": true,
    "verify": true,
    "enabled": true,
    "note": null,
    "createdAt": "2026-10-27T20:10:10.345Z",
    "updatedAt": "2026-10-27T20:10:10.345Z"
  }
}

No url: a mailbox

curl -X POST "https://api.ardaform.net/v1/webhooks" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Docs mailbox","slug":"zz-docs-mailbox","note":"Check-ins, read by the nightly job"}'
Answer · 201 Createdran live 2026-10-27 · test project
{
  "webhook": {
    "slug": "zz-docs-mailbox",
    "name": "Docs mailbox",
    "url": null,
    "method": "post",
    "trigger": "app",
    "dataset": null,
    "debounceSeconds": 2,
    "hasKey": false,
    "verify": false,
    "enabled": true,
    "note": "Check-ins, read by the nightly job",
    "createdAt": "2026-10-27T20:10:10.248Z",
    "updatedAt": "2026-10-27T20:10:10.248Z"
  }
}

Past the package's webhook endpoints

curl -X POST "https://api.ardaform.net/v1/webhooks" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"A third","slug":"zz-docs-third"}'
Answer · 403 Forbiddenran live 2026-10-27 · test project
{
  "error": {
    "code": "LIMIT_REACHED",
    "message": "Your package allows 2 webhook endpoints, and you have 2.",
    "limit": 2,
    "used": 2
  }
}

An address on a private network

curl -X POST "https://api.ardaform.net/v1/webhooks" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"On the LAN","slug":"zz-docs-lan","url":"http://192.168.0.10/hook"}'
Answer · 400 Bad Requestran live 2026-10-27 · test project
{
  "error": {
    "code": "BAD_URL",
    "message": "A webhook cannot point at a private, loopback or link-local address"
  }
}

GET/v1/webhooks/{slug}One webhook

One webhook's definition.

Key scope webhooks:r — read webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.

Answer 200 The webhook

FieldType
webhookWebhookAn HTTP endpoint of yours that the app on a device may send a message to — Arda.webhook.call(slug, data). The plane makes the call, signs it and records every delivery; the device never holds your key and never talks to your server directly.

Errors

StatusWordWhen
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
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/webhooks/zz-docs-receiver" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhook": {
    "slug": "zz-docs-receiver",
    "name": "Docs receiver",
    "url": "https://ardaform.net/health",
    "method": "post",
    "trigger": "app",
    "dataset": null,
    "debounceSeconds": 2,
    "hasKey": true,
    "verify": true,
    "enabled": true,
    "note": null,
    "createdAt": "2026-10-27T20:10:10.345Z",
    "updatedAt": "2026-10-27T20:10:10.345Z"
  }
}

PATCH/v1/webhooks/{slug}Change a webhook

Changes what is sent. "key": null clears the key; "url": null makes the webhook a mailbox; "enabled": false refuses the app's calls on the device until it is switched back on.

Key scope webhooks:w — write webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.

Body

FieldType
namestring
at least 1 character · at most 120 characters
For people. Trimmed; at most 120 characters.
urlstring, nullable
at most 1024 characters
http or https, at most 1024 characters, a PUBLIC host: localhost and loopback, private and link-local addresses are 400 BAD_URL (and refused again by the plane at call time, after the name resolves). Absent, null or "" makes a webhook that only RECORDS its calls (url: null in the answer).
methodstring — post or get or putpost (the default) and put send the envelope as a JSON body; get sends it as query parameters.
triggerstring — app or datasetapp (the default): the device application calls it. dataset: the plane calls it when records of dataset change, debounceSeconds after the last change, one call per window.
datasetstring, nullableFor trigger: dataset: the key of one of this project's datasets. 400 BAD_REQUEST for a key the project has not got.
debounceSecondsinteger
0–300
For trigger: dataset: the wait after a change for more, before the one call. Default 2.
keystring, nullable
at most 255 characters
WRITE-ONLY, at most 255 characters. Sent as Authorization: Bearer and used to sign every delivery (X-Arda-Signature). Never returned — read hasKey. On PATCH, null or "" clears it.
verifybooleanMint a one-time X-Arda-Verify token per delivery, for POST /v1/webhooks/verify. Default false; no effect while there is no url. JSON true/false (or 1/0) — a string is refused.
enabledbooleanDefault true. Off, the app's calls are refused on the device and nothing is sent. JSON true/false (or 1/0).
notestring, nullable
at most 255 characters
Whatever helps the next person. At most 255 characters; null clears it.
slugstringAccepted only if it is the slug already — anything else is SLUG_IMMUTABLE.

Answer 200 Saved

FieldType
webhookWebhookAn HTTP endpoint of yours that the app on a device may send a message to — Arda.webhook.call(slug, data). The plane makes the call, signs it and records every delivery; the device never holds your key and never talks to your server directly.

Errors

StatusWordWhen
400SLUG_IMMUTABLEA PATCH sent a different slug. A slug never changes — your app addresses the webhook by it.
400BAD_REQUESTThe 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.
400BAD_URLThe url is not http or https, is over 1024 characters, does not parse, or names localhost, a loopback, private or link-local address.
400NOTHING_TO_DOA PATCH sent no field it can change (a dataset with none of its fields; a device policy with no key).
403NO_WEBHOOKSThe account's package does not include webhooks. Reads still work.
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
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 BAD_JSON TOO_LARGE.

curl -X PATCH "https://api.ardaform.net/v1/webhooks/zz-docs-receiver" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note":"Our test receiver","enabled":true}'
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhook": {
    "slug": "zz-docs-receiver",
    "name": "Docs receiver",
    "url": "https://ardaform.net/health",
    "method": "post",
    "trigger": "app",
    "dataset": null,
    "debounceSeconds": 2,
    "hasKey": true,
    "verify": true,
    "enabled": true,
    "note": "Our test receiver",
    "createdAt": "2026-10-27T20:10:10.345Z",
    "updatedAt": "2026-10-27T20:10:11.064Z"
  }
}

Trying to rename it

curl -X PATCH "https://api.ardaform.net/v1/webhooks/zz-docs-receiver" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"receiver"}'
Answer · 400 Bad Requestran live 2026-10-27 · test project
{
  "error": {
    "code": "SLUG_IMMUTABLE",
    "message": "A webhook slug cannot be changed; your app addresses the webhook by it."
  }
}

DELETE/v1/webhooks/{slug}Delete a webhook and its delivery log

Deletes the webhook and its delivery log, and says how many deliveries went. The app's calls to the slug are refused from then on.

Key scope webhooks:d — delete webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.

Answer 200 Deleted

FieldType
deletedobject
deleted.webhookstringThe slug.
deleted.deliveriesintegerHow many log rows went with it.

Errors

StatusWordWhen
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
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 -X DELETE "https://api.ardaform.net/v1/webhooks/zz-docs-mailbox" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "deleted": {
    "webhook": "zz-docs-mailbox",
    "deliveries": 1
  }
}

POST/v1/webhooks/{slug}/testSend a test delivery now

Makes one delivery now, exactly as a device's call would — same headers, same signature, same log row — with device: null and data: {"test": true, "at": …}. The answer is what your server said.

  • ok: false with status: 0 is an answer about your server: unreachable, refused, timed out (10 seconds). error says which.
  • 502 CONNECT_UNAVAILABLE is about ours — nothing was sent; try again shortly. A disabled webhook is 409 WEBHOOK_DISABLED.

Key scope webhooks:w — write webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.

Answer 200 What happened

FieldType
deliveryobject
delivery.idstring, nullableThe delivery id, also in the log.
delivery.okbooleanWhether your server answered 2xx (always true with no url).
delivery.statusintegerYour server's status. 0 when it was never reached; 202 with no url.
delivery.msintegerHow long the call took.
delivery.bodystring, nullableThe first 1024 characters of what your server answered. Null when there was none.
delivery.errorstring, nullableWhy status is 0.

Errors

StatusWordWhen
400BAD_REQUESTThe 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.
403NO_WEBHOOKSThe account's package does not include webhooks. Reads still work.
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
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.
409WEBHOOK_DISABLEDA Test on a webhook that is switched off.
502CONNECT_UNAVAILABLEThe 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.

curl -X POST "https://api.ardaform.net/v1/webhooks/zz-docs-receiver/test" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "delivery": {
    "id": "dlv_4233a8826b4d795b",
    "ok": true,
    "status": 200,
    "ms": 104,
    "body": "ok\n",
    "error": null
  }
}

On a mailbox

curl -X POST "https://api.ardaform.net/v1/webhooks/zz-docs-mailbox/test" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "delivery": {
    "id": "dlv_89e7996d7812e06c",
    "ok": true,
    "status": 202,
    "ms": 0,
    "body": null,
    "error": null
  }
}

GET/v1/webhooks/{slug}/deliveriesThe delivery log — newest first, or a feed after one delivery

One row per call: when, which device, what your server answered and how long it took. Kept for 500 deliveries or seven days per webhook, and not counted against storage.

  • Without after, newest first — the log as a person reads it.
  • With after=dlv_…, the deliveries recorded after that one, oldest first: a feed. Keep the id of the last one you handled and ask again; you never miss one or see one twice. A delivery joins the feed once it is two seconds old, so two calls finishing at once cannot leave a gap behind your cursor.
  • An after the log no longer holds is 404 NO_DELIVERY, not an empty page: you fell behind by more than the log keeps.
  • include=payload adds each call's envelope — the app's data — and caps a page at 50.

Key scope webhooks:r — read webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.

Query parameters

NameType
limitinteger
1–200 · default 50
1–200, default 50 — at most 50 with include=payload. More is cut to the cap; 0 or not a number is 50.
includestring — payloadpayload puts the envelope on every delivery. Any other value is 400 BAD_PARAM.
afterstring
pattern ^dlv_[0-9a-f]{16}$
A delivery id of this webhook. Answers the deliveries recorded after it, oldest first. Not of the form: 400 BAD_PARAM; not this webhook's or pruned: 404 NO_DELIVERY.

Answer 200 The log

FieldType
webhookstringThe slug.
limitintegerThe page size used.
hasMorebooleanThere are more beyond this page — older ones without after, newer ones ready now with it.
deliveriesarray of Webhook delivery

Errors

StatusWordWhen
400BAD_PARAMA query parameter the endpoint does not take, or a value it does not know. Refused rather than ignored, so a typo in a filter never returns everything.
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
404NO_DELIVERYNo such delivery: not this webhook's, never made, older than the log keeps (500 rows or seven days), or — for /verify — older than 24 hours.
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/webhooks/zz-docs-receiver/deliveries?limit=20" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhook": "zz-docs-receiver",
  "limit": 20,
  "hasMore": false,
  "deliveries": [
    {
      "id": "dlv_4233a8826b4d795b",
      "at": "2026-10-27T20:10:11.445Z",
      "device": null,
      "status": 200,
      "ok": true,
      "ms": 104,
      "error": null,
      "verified": false,
      "verifiedAt": null
    }
  ]
}

With include=payload, on the mailbox

curl "https://api.ardaform.net/v1/webhooks/zz-docs-mailbox/deliveries?include=payload" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhook": "zz-docs-mailbox",
  "limit": 50,
  "hasMore": false,
  "deliveries": [
    {
      "id": "dlv_89e7996d7812e06c",
      "at": "2026-10-27T20:10:11.257Z",
      "device": null,
      "status": 202,
      "ok": true,
      "ms": 0,
      "error": null,
      "verified": false,
      "verifiedAt": null,
      "payload": {
        "id": "dlv_89e7996d7812e06c",
        "webhook": "zz-docs-mailbox",
        "project": "api-test",
        "device": null,
        "at": "2026-10-27T20:10:11.257Z",
        "verify": false,
        "data": {
          "test": true,
          "at": "2026-10-27T20:10:11.257Z"
        }
      }
    }
  ]
}

After the last one read: nothing new yet

curl "https://api.ardaform.net/v1/webhooks/zz-docs-mailbox/deliveries?include=payload&after=dlv_89e7996d7812e06c" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "webhook": "zz-docs-mailbox",
  "limit": 50,
  "hasMore": false,
  "deliveries": []
}

GET/v1/webhooks/{slug}/deliveries/{id}One delivery, with what was sent and what came back

One delivery with what was sent (payload, the envelope) and the start of what came back (response, 1024 characters). The id is the X-Arda-Delivery your server saw.

Key scope webhooks:r — read webhooks

Path parameters

NameType
slugstring
pattern ^[a-z0-9][a-z0-9_-]{0,79}$
The webhook's slug — what your app says in Arda.webhook.call(slug, …), not an id.
idstring
pattern ^dlv_[0-9a-f]{16}$
The delivery id — dlv_ and sixteen hex characters: the X-Arda-Delivery your server saw, and the id in the log. Anything else is 404 NO_DELIVERY.

Answer 200 The delivery

FieldType
deliveryWebhook deliveryOne call the plane made to your server — the same id your server saw as X-Arda-Delivery — or, on a webhook with no url, one call it recorded instead of sending. The plane keeps the most recent ones per webhook (500 rows or seven days), never counted against your storage.

Errors

StatusWordWhen
400BAD_PARAMA query parameter the endpoint does not take, or a value it does not know. Refused rather than ignored, so a typo in a filter never returns everything.
403OUT_OF_SCOPEThe 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.
404NO_WEBHOOKNo webhook with that slug on the key's project.
404NO_DELIVERYNo such delivery: not this webhook's, never made, older than the log keeps (500 rows or seven days), or — for /verify — older than 24 hours.
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/webhooks/zz-docs-receiver/deliveries/dlv_4233a8826b4d795b" \
  -H "Authorization: Bearer $ARDA_KEY"
Answer · 200 OKran live 2026-10-27 · test project
{
  "delivery": {
    "id": "dlv_4233a8826b4d795b",
    "at": "2026-10-27T20:10:11.445Z",
    "device": null,
    "status": 200,
    "ok": true,
    "ms": 104,
    "error": null,
    "verified": false,
    "verifiedAt": null,
    "payload": {
      "id": "dlv_4233a8826b4d795b",
      "webhook": "zz-docs-receiver",
      "project": "api-test",
      "device": null,
      "at": "2026-10-27T20:10:11.341Z",
      "verify": true,
      "data": {
        "test": true,
        "at": "2026-10-27T20:10:11.341Z"
      }
    },
    "response": "ok\n"
  }
}

A mailbox

A webhook with no url needs no server of yours that the internet can reach: every call the app makes is recorded, and your system collects them when it likes. Read the feed from the last id you handled, oldest first, and store the cursor after the work is done:

const URL = 'https://api.ardaform.net/v1/webhooks/checkins/deliveries'
const auth = { Authorization: `Bearer ${process.env.ARDA_KEY}` }

// after: the id of the last delivery you handled, or null the first time
export async function drain(after, handle) {
  for (;;) {
    const q = new URLSearchParams({ include: 'payload', limit: '50' })
    if (after) q.set('after', after)
    const res = await fetch(`${URL}?${q}`, { headers: auth })
    const page = await res.json()
    if (res.status === 404 && page.error?.code === 'NO_DELIVERY') {
      after = null                         // fell behind the log: start from the newest
      continue
    }
    if (!res.ok) throw new Error(page.error.code)
    // without `after` the page is newest first; with it, oldest first
    const list = after ? page.deliveries : [...page.deliveries].reverse()
    for (const d of list) {
      await handle(d.payload.data, d.payload.device, d.id)   // key your work on d.id
      after = d.id                         // save it after the work, not before
    }
    if (!page.hasMore || !list.length) return after
  }
}

A pattern rather than a captured call: not run as a whole; each request in it is the one above, which was. The log keeps 500 deliveries or seven days — a mailbox is a queue to drain, not an archive.

POST/v1/webhooks/verifyProve a delivery came from ArdaForm

For your server, on a webhook with verify on: send back the X-Arda-Delivery id and the X-Arda-Verify token a delivery carried, and learn whether ArdaForm really made that call. Works for 24 hours, and only with a key of the same project.

  • {"valid": false} is an answer: that call did not come from ArdaForm.
  • With valid: true, verifiedAt is from before this call: null the first time a token is presented, a time when it was presented already — a retry of yours, or a replay — and your server decides.
  • A valid: true answer needs the token from a real delivery, which only your server sees; the examples could not capture one. Its fields are in the table.

Key scope webhooks:r — read webhooks

Body

FieldType
delivery requiredstring
pattern ^dlv_[0-9a-f]{16}$
The X-Arda-Delivery header.
token requiredstring
at least 1 character
The X-Arda-Verify header, exactly as it arrived.

Answer 200 The verdict

FieldType
validbooleanWhether the token matches that delivery. False is an answer: the plane did not make that call. The other fields are present only when true.
webhookstringThe slug.
deviceobject, nullableThe device whose app sent it, {uid, name}, or null for a Test.
device.uidstringThe device's public id.
device.namestringThe device's name.
atstring (date-time)When the plane made the call.
statusintegerThe HTTP status your server answered, as the plane recorded it.
verifiedAtstring (date-time), nullableWhen this token was FIRST verified, from before this call — null the first time.

Errors

StatusWordWhen
400BAD_REQUESTThe 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.
400NOT_VERIFIABLEThe delivery's webhook has verify off (or no url), so the delivery carried no token.
403OUT_OF_SCOPEThe 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.
404NO_DELIVERYNo such delivery: not this webhook's, never made, older than the log keeps (500 rows or seven days), or — for /verify — older than 24 hours.
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 BAD_JSON TOO_LARGE.

A token that does not match

curl -X POST "https://api.ardaform.net/v1/webhooks/verify" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"delivery":"dlv_4233a8826b4d795b","token":"0000000000000000000000000000000000000000000000000000000000000000"}'
Answer · 200 OKran live 2026-10-27 · test project
{
  "valid": false
}

A delivery of a webhook without verify

curl -X POST "https://api.ardaform.net/v1/webhooks/verify" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"delivery":"dlv_89e7996d7812e06c","token":"0000000000000000000000000000000000000000000000000000000000000000"}'
Answer · 400 Bad Requestran live 2026-10-27 · test project
{
  "error": {
    "code": "NOT_VERIFIABLE",
    "message": "Webhook 'zz-docs-mailbox' has verify switched off, so its deliveries carry no token.",
    "webhook": "zz-docs-mailbox"
  }
}

An id ArdaForm never sent

curl -X POST "https://api.ardaform.net/v1/webhooks/verify" \
  -H "Authorization: Bearer $ARDA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"delivery":"dlv_0000000000000000","token":"0000000000000000000000000000000000000000000000000000000000000000"}'
Answer · 404 Not Foundran live 2026-10-27 · test project
{
  "error": {
    "code": "NO_DELIVERY",
    "message": "No delivery with that id in this project in the last 24 hours."
  }
}

Fired by a dataset

A webhook does not have to wait for the app. Create or patch one with "trigger": "dataset", the key of one of the project's datasets as dataset, and a debounceSeconds (default 2), and the plane calls it whenever records of that dataset change — a device pushed some, this API wrote some — waiting the given seconds for more and making one call for the whole window. It is an ordinary delivery: the same envelope, signature, limits and delivery log; device is null and data says what moved.

"data": {"event": "dataset.changed", "dataset": "orders", "name": "Orders",
         "records": 3, "devices": ["7f3a…"], "since": "2026-10-25T20:45:50.544Z", "until": "2026-10-25T20:45:51.547Z"}

records is how many moved (0 when a writer did not say), devices the devices that pushed (empty for this API's writes), since and until the window. A call the account's per-minute limit refuses is dropped, not queued: the next change opens a new window, and your server can always read the dataset itself. Deleting the dataset leaves the webhook silent (dataset: null); re-point it with a PATCH.

The shapes

Webhook

An HTTP endpoint of yours that the app on a device may send a message to — Arda.webhook.call(slug, data). The plane makes the call, signs it and records every delivery; the device never holds your key and never talks to your server directly.

FieldType
slugstringHow the app addresses it, and what your code says. Lower-case letters, digits, hyphen and underscore, starting with a letter or digit. FROZEN after create — verify is reserved.
namestringFor people. Change it freely.
urlstring, nullableWhere the plane sends. http or https on a public host: localhost and loopback, link-local and private addresses are refused when saved, and refused again by the plane at call time after the name resolves. NULL FOR A WEBHOOK THAT ONLY RECORDS ITS CALLS: nothing is sent, the app is answered ok with status 202, and every call is a delivery whose envelope …/deliveries hands back — a mailbox your own systems poll.
methodstring — post or get or putHow it is sent. post and put carry the envelope as a JSON body; get carries the same fields as query parameters.
triggerstring — app or datasetWhat makes the call. app: the device application calls it (Arda.webhook.call). dataset: THE PLANE calls it when records of dataset change — a device pushed some, the API wrote some — debounceSeconds after the last change, ONE call for every change in that window; device is null in the envelope and data is {event: "dataset.changed", dataset, name, records, devices, since, until}.
datasetstring, nullableFor trigger: dataset: the key of the dataset watched, one of this project's. Null for an app webhook — and null when the dataset it watched was deleted, which leaves the webhook silent.
debounceSecondsintegerFor trigger: dataset: how long the plane waits after a change for more before the one call. 0 to 300; default 2.
hasKeybooleanWhether a key is set. THE KEY ITSELF IS NEVER RETURNED: set it with key on create or PATCH, clear it with "key": null. When set, every delivery carries it as Authorization: Bearer and is signed with it (X-Arda-Signature).
verifybooleanMint a one-time X-Arda-Verify token per delivery, which your server hands back to POST /v1/webhooks/verify to prove the call came from here. Off by default. No effect on a webhook with no url — nothing is sent, so there is nobody to hand a token to.
enabledbooleanOff, and the app's call is refused on the device and nothing is sent. The delivery log is kept.
notestring, nullableOptional. Whatever helps the next person.
createdAtstring (date-time)When it was made. UTC.
updatedAtstring (date-time)When its definition last changed. A delivery does not move it. UTC.

Webhook delivery

One call the plane made to your server — the same id your server saw as X-Arda-Delivery — or, on a webhook with no url, one call it recorded instead of sending. The plane keeps the most recent ones per webhook (500 rows or seven days), never counted against your storage.

FieldType
idstringThe delivery id: dlv_ and sixteen hex characters. What X-Arda-Delivery and the envelope's id carried, and what /verify takes.
atstring (date-time)When the plane made the call. UTC.
deviceobject, nullableThe device whose app sent it, or null for a Test run from the portal or from POST /v1/webhooks/{slug}/test.
device.uidstringThe device's public id, as the portal shows it.
device.namestringThe device's name.
statusintegerThe HTTP status your server answered. 0 means it was never reached — error says why. On a webhook with no url it is always 202: recorded, nothing sent.
okbooleanWhether status was 2xx.
msintegerHow long the call took, in milliseconds. A timeout counts the whole wait.
errorstring, nullableWhy status is 0: a refused address, a timeout, TLS, a connection fault. Null when your server answered.
verifiedbooleanWhether your server has presented this delivery's token to /verify and been answered valid: true. Always false for a webhook with verify off.
verifiedAtstring (date-time), nullableWhen that first happened, or null.
payloadobject, nullableThe envelope as the plane built it — what your server was sent, or what a webhook with no url recorded: id, webhook, project, device, at, verify and data, the app's own JSON untouched. ONLY on GET …/deliveries/{id}, and on the list with include=payload. Null for a delivery recorded before 2026-10-06, which kept none.
responsestring, nullableThe first 1024 characters of what your server answered. ONLY on GET …/deliveries/{id}. Null when it answered nothing, was never reached, or the webhook has no url.