Webhooks

Being told when something happens, instead of asking.

Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.

GET/webhooks

List webhooksviewer+

Every webhook subscription on the account, with its health. No secret is ever in this list — the secret is shown once, when the subscription is created or its secret rotated.

An API key issued against a workspace sees the subscriptions confined to that workspace only.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/webhooks' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    result = client.webhooks.list()
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const result = await client.webhooks.list();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

[
  {
    "id": "whk-e1f2a3b4c5d6e7f8",
    "url": "https://example.com/mandala-events",
    "description": "Desktop readiness notifications",
    "events": [
      "computer.ready"
    ],
    "computers": [],
    "enabled": true,
    "disabled_reason": null,
    "disabled_at": null,
    "last_success_at": null,
    "last_failure_at": null,
    "last_status": null,
    "created_at": "2026-09-16T12:00:00.000Z",
    "updated_at": "2026-09-16T12:00:00.000Z"
  }
]

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Response

The subscriptions, oldest first.

Response fields

idalwaysstring

whk- and sixteen hex characters.

urlalwaysstring

Where deliveries are POSTed. https:// only. The URL as the platform parsed it, which can differ in spelling from what you sent: the host lower-cased, a default port dropped, a bare origin ending in /.

descriptionalwaysstring

Free text, for your listing. Empty when you gave none.

eventsalwaysstring[]

The event types this subscription receives. Empty means every type. The vocabulary is the event socket’s (the computer’s events_url) less file.changed: window.opened, window.closed, window.focused, window.blurred, clipboard.changed, process.exited, computer.idle, computer.ready, computer.started, computer.stopped, computer.suspended.

computersalwaysstring[]

The computer ids this subscription receives events for. Empty means every computer in scope. Not checked against your computers when you set it — a subscription may name a computer you are about to create.

enabledalwaysboolean

Whether deliveries are made. Set false by the platform when an endpoint has failed for a day, or when the account moves to a plan without webhooks — see disabled_reason. Back to true by you with PATCH, which starts fresh, or, for plan, by the platform when you choose a plan again, which starts fresh too.

disabled_reasonalways"customer" | "failing" | "plan" | "null"

Why enabled is false: customer when you disabled it, failing when the platform did — a delivery ran out of attempts and nothing had been accepted for 24 hours, counted from the later of the last 2xx and when the subscription was last enabled — and plan when the account moved to a plan without webhooks. A plan subscription is enabled again, oldest first, as many as the plan allows, when the account chooses a plan that includes webhooks. null while enabled.

disabled_atalwaysstring or null

RFC 3339 timestamp, or null while enabled.

last_success_atalwaysstring or null

When the endpoint last answered 2xx to any delivery. null until it has.

last_failure_atalwaysstring or null

When a delivery attempt last failed. null until one has.

last_statusalwaysinteger or null

The HTTP status of the newest attempt, whatever it was. null before any attempt, or when the newest got no answer.

workspace_idstring

The workspace this subscription is confined to. Present only when it was created with a workspace-scoped API key; absent on an account-wide subscription.

created_atalwaysstring

RFC 3339 timestamp.

updated_atalwaysstring

RFC 3339 timestamp.

Failures

403

A suspended account may still make this read: a 403 here is about the credential or the person holding it, never the account’s suspension.

POST/webhooks

Create a webhookmember+

Subscribe an HTTPS endpoint to this account’s events. The answer carries the signing secret once; it is not readable again, and POST /webhooks/{id}/rotate is how you get a new one.

WHAT ARRIVES. A POST per event to url, whose body is the event object exactly as the computer’s event socket (its events_url) frames it — type, at, computer, seq, cursor, source, data — byte for byte, with nothing added and nothing wrapped around it. cursor is the bridge back to the stream: a job woken by process.exited that wants everything since can open the computer’s events_url with since= that cursor added. computer says which machine, and every subscription may receive events from many. Two bodies are not events and lack fields: a gap (below) has no seq, and a test delivery (type: "webhook.test", from POST /webhooks/{id}/test) has neither seq nor cursor. data is left out of an event that has none. WebhookEvent in the OpenAPI document (its top-level webhooks) is all three as a schema.

HEADERS. content-type: application/json, user-agent: Mandala-Webhooks/1, and the three [Standard Webhooks](https://www.standardwebhooks.com) headers: webhook-id (the delivery id, unchanged across retries), webhook-timestamp (Unix seconds, the time of THIS attempt) and webhook-signature (v1, and base64 of HMAC-SHA256 over <id>.<timestamp>.<raw body>, keyed by the secret’s bytes after whsec_, base64-decoded). Any Standard Webhooks verifier library checks it; verify the RAW request bytes, never a re-serialised body. Refuse a webhook-timestamp more than 300 seconds from your clock, and remember each webhook-id you accept for at least that long — together those two close every replay, and a retry of a delivery you already accepted is then recognised rather than processed twice. mandala-subscription names the subscription and is NOT signed: route on it, never authorise on it.

ACKNOWLEDGE WITH A 2xx BEFORE DOING THE WORK. An attempt is cut at 10 seconds and counted as a failure; anything else — a non-2xx, a timeout, a refused connection, a TLS error, a redirect (never followed) — is retried: eight attempts over about fourteen hours (30 s, 2 min, 10 min, 30 min, 1 h, 4 h, 8 h), then the delivery is exhausted and visible in GET /webhooks/{id}/deliveries. Retries carry the same webhook-id and a fresh timestamp and signature. No ordering is promised: deliveries for one subscription run four at a time and retries interleave with new events, so order by seq per computer if you care. At least once, never silently dropped.

AN ENDPOINT THAT KEEPS FAILING IS DISABLED: when a delivery runs out of attempts and nothing has been accepted for 24 hours, enabled becomes false with disabled_reason: "failing", pending deliveries are dropped, and PATCH {"enabled": true} starts it again. The 24 hours count from the later of the last 2xx and when the subscription was last enabled: created enabled, enabled again with PATCH, or turned back on by a plan. So an endpoint enabled again gets a fresh 24 hours, not the remainder of a day that began at its last 2xx.

WHEN THE PLAN ENDS. An account that moves to a plan without webhooks has every enabled subscription disabled with disabled_reason: "plan". Deliveries already queued are still made, and so is one more kind for the next 24 hours: computer.stopped, so you hear about the computers stopped because the plan ended (subject to the subscription’s own events and computers). Nothing else is queued. Until the account has a plan again, a new url, enabled: true and a test are 402; reading, filters, rotating and deleting still work. Choosing a plan that includes webhooks enables them again, oldest first, as many as it allows, each starting fresh as a PATCH would; the rest stay disabled with disabled_reason: "plan".

GAPS. If the hypervisor holding a computer was unreachable for longer than its event journal holds, you get a {"type": "gap", "computer": …} delivery whose data.detail says events were lost and whose cursor is the computer’s own — the same frame the socket sends, and it arrives whatever events filters on. Reconcile with a listing rather than assume nothing happened.

Every paid plan allows ten subscriptions per account; the eleventh is a 409 naming the cap. The count is the whole account’s, so an API key confined to a workspace can meet the cap while GET /webhooks shows it fewer than ten.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/webhooks' \
  -H "Authorization: Bearer $MANDALA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-raw '{"url":"https://ci.example.com/mandala","events":["process.exited","computer.ready"]}'

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    body = json.loads("{\"url\":\"https://ci.example.com/mandala\",\"events\":[\"process.exited\",\"computer.ready\"]}")
    result = client.webhooks.create(body["url"], events=body["events"])
    print("Secret returned once; store result.secret securely without logging it.")

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const body = {"url":"https://ci.example.com/mandala","events":["process.exited","computer.ready"]};
const result = await client.webhooks.create(body);
console.log("Secret returned once; store result.secret securely without logging it.");

201 Illustrative successful response.

HTTP 201
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "whk-e1f2a3b4c5d6e7f8",
  "url": "https://ci.example.com/mandala",
  "description": "",
  "events": [
    "process.exited",
    "computer.ready"
  ],
  "computers": [],
  "enabled": true,
  "disabled_reason": null,
  "disabled_at": null,
  "last_success_at": null,
  "last_failure_at": null,
  "last_status": null,
  "created_at": "2026-09-16T12:00:00.000Z",
  "updated_at": "2026-09-16T12:00:00.000Z",
  "secret": "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Request body

urlrequiredstring

Where to POST. https:// only, no username or password in it, at most 2048 characters — counted on the string you send, before it is parsed, so a non-ASCII character may count as two — and it must resolve to a public address — a hostname whose answers include a private, loopback, link-local or otherwise reserved address is refused, and so is a literal one. A port other than 443 is fine. The name is looked up when you send it: one that does not resolve then is a 400 (<host> did not resolve) even when the cause is a passing DNS failure, so a name you have not published yet cannot be registered, and the same request works once it resolves. What is stored is the URL as parsed: the host lower-cased, a default port dropped, and a / added to a bare origin, so compare against the stored url rather than what you sent.

descriptionstring

Free text for your listing, up to 200 characters.

events"window.opened" | "window.closed" | "window.focused" | "window.blurred" | "clipboard.changed" | "process.exited" | "computer.idle" | "computer.ready" | "computer.started" | "computer.stopped" | "computer.suspended"[]

Event types to deliver, from the vocabulary on Webhook. Omit or send [] for every type. An unknown type is a 400 that lists them.

computersstring[]

Computer ids to deliver for, up to 64, each a non-empty string of at most 64 characters. Omit or send [] for every computer in scope.

enabledboolean

Start it disabled with false, to enable later. Defaults to true.

Response

The subscription, with its secret — shown once.

Response fields

idalwaysstring

whk- and sixteen hex characters.

urlalwaysstring

Where deliveries are POSTed.

descriptionalwaysstring
eventsalwaysstring[]

As on Webhook.

computersalwaysstring[]

As on Webhook.

enabledalwaysboolean
disabled_reasonalwaysstring or null
disabled_atalwaysstring or null
last_success_atalwaysstring or null
last_failure_atalwaysstring or null
last_statusalwaysinteger or null
workspace_idstring

As on Webhook: present only on a workspace-confined subscription.

created_atalwaysstring

RFC 3339 timestamp.

updated_atalwaysstring

RFC 3339 timestamp.

secretalwaysstring

The signing secret: whsec_ and 44 characters of base64. Shown here and never again — store it now. Every delivery is signed with it; see the resource description for how to verify.

Failures

413

This route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.

GET/webhooks/{id}

Read a webhookviewer+

One subscription, with its health: when the endpoint last accepted a delivery, when one last failed, the status of the newest attempt, and whether the platform has disabled it. Never the secret.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    result = client.webhooks.get(id)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const result = await client.webhooks.get(id);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "whk-e1f2a3b4c5d6e7f8",
  "url": "https://example.com/mandala-events",
  "description": "Desktop readiness notifications",
  "events": [
    "computer.ready"
  ],
  "computers": [],
  "enabled": true,
  "disabled_reason": null,
  "disabled_at": null,
  "last_success_at": null,
  "last_failure_at": null,
  "last_status": null,
  "created_at": "2026-09-16T12:00:00.000Z",
  "updated_at": "2026-09-16T12:00:00.000Z"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PATCH, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Response

The subscription.

Response fields

idalwaysstring

whk- and sixteen hex characters.

urlalwaysstring

Where deliveries are POSTed. https:// only. The URL as the platform parsed it, which can differ in spelling from what you sent: the host lower-cased, a default port dropped, a bare origin ending in /.

descriptionalwaysstring

Free text, for your listing. Empty when you gave none.

eventsalwaysstring[]

The event types this subscription receives. Empty means every type. The vocabulary is the event socket’s (the computer’s events_url) less file.changed: window.opened, window.closed, window.focused, window.blurred, clipboard.changed, process.exited, computer.idle, computer.ready, computer.started, computer.stopped, computer.suspended.

computersalwaysstring[]

The computer ids this subscription receives events for. Empty means every computer in scope. Not checked against your computers when you set it — a subscription may name a computer you are about to create.

enabledalwaysboolean

Whether deliveries are made. Set false by the platform when an endpoint has failed for a day, or when the account moves to a plan without webhooks — see disabled_reason. Back to true by you with PATCH, which starts fresh, or, for plan, by the platform when you choose a plan again, which starts fresh too.

disabled_reasonalways"customer" | "failing" | "plan" | "null"

Why enabled is false: customer when you disabled it, failing when the platform did — a delivery ran out of attempts and nothing had been accepted for 24 hours, counted from the later of the last 2xx and when the subscription was last enabled — and plan when the account moved to a plan without webhooks. A plan subscription is enabled again, oldest first, as many as the plan allows, when the account chooses a plan that includes webhooks. null while enabled.

disabled_atalwaysstring or null

RFC 3339 timestamp, or null while enabled.

last_success_atalwaysstring or null

When the endpoint last answered 2xx to any delivery. null until it has.

last_failure_atalwaysstring or null

When a delivery attempt last failed. null until one has.

last_statusalwaysinteger or null

The HTTP status of the newest attempt, whatever it was. null before any attempt, or when the newest got no answer.

workspace_idstring

The workspace this subscription is confined to. Present only when it was created with a workspace-scoped API key; absent on an account-wide subscription.

created_atalwaysstring

RFC 3339 timestamp.

updated_atalwaysstring

RFC 3339 timestamp.

Failures

403

A suspended account may still make this read: a 403 here is about the credential or the person holding it, never the account’s suspension.

PATCH/webhooks/{id}

Update a webhookmember+

Change the endpoint, the description, the filters, or enabled. Fields you omit are left as they are; a body that names none of them is a 400.

A new url is checked exactly as on create. enabled: true clears a failing disable and starts fresh: the 24 hours before it can be disabled for failing again count from now, not from its last 2xx. Sent to a subscription that is already enabled, it does not restart them. enabled: false stops deliveries and records that you chose to. It is not a pause that holds events for later: most events while it is disabled are never queued, and a queued delivery whose next attempt falls due while it is disabled is dropped. Nor is it a clean cut: a retry that was not yet due, or an event that reaches the platform late, can still be delivered after you enable it again. Read the computer’s events_url with since= to reconcile, and dedupe on webhook-id.

While the account’s plan includes no webhooks, a body with a url or with enabled: true is a 402 and nothing changes; the description, the filters and enabled: false can still be set.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X PATCH "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8' \
  -H "Authorization: Bearer $MANDALA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-raw '{"enabled":true}'

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    body = json.loads("{\"enabled\":true}")
    result = client.webhooks.update(id, **body)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const body = {"enabled":true};
const result = await client.webhooks.update(id, body);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "whk-e1f2a3b4c5d6e7f8",
  "url": "https://example.com/mandala-events",
  "description": "Desktop readiness notifications",
  "events": [
    "computer.ready"
  ],
  "computers": [],
  "enabled": true,
  "disabled_reason": null,
  "disabled_at": null,
  "last_success_at": null,
  "last_failure_at": null,
  "last_status": null,
  "created_at": "2026-09-16T12:00:00.000Z",
  "updated_at": "2026-09-16T12:00:01.000Z"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PATCH, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Request body

urlstring

Where to POST. https:// only, no username or password in it, at most 2048 characters — counted on the string you send, before it is parsed, so a non-ASCII character may count as two — and it must resolve to a public address — a hostname whose answers include a private, loopback, link-local or otherwise reserved address is refused, and so is a literal one. A port other than 443 is fine. The name is looked up when you send it: one that does not resolve then is a 400 (<host> did not resolve) even when the cause is a passing DNS failure, so a name you have not published yet cannot be registered, and the same request works once it resolves. What is stored is the URL as parsed: the host lower-cased, a default port dropped, and a / added to a bare origin, so compare against the stored url rather than what you sent.

descriptionstring

Free text for your listing, up to 200 characters.

events"window.opened" | "window.closed" | "window.focused" | "window.blurred" | "clipboard.changed" | "process.exited" | "computer.idle" | "computer.ready" | "computer.started" | "computer.stopped" | "computer.suspended"[]

Event types to deliver, from the vocabulary on Webhook. Omit or send [] for every type. An unknown type is a 400 that lists them.

computersstring[]

Computer ids to deliver for, up to 64, each a non-empty string of at most 64 characters. Omit or send [] for every computer in scope.

enabledboolean

Whether deliveries are made.

Response

The subscription as stored.

Response fields

idalwaysstring

whk- and sixteen hex characters.

urlalwaysstring

Where deliveries are POSTed. https:// only. The URL as the platform parsed it, which can differ in spelling from what you sent: the host lower-cased, a default port dropped, a bare origin ending in /.

descriptionalwaysstring

Free text, for your listing. Empty when you gave none.

eventsalwaysstring[]

The event types this subscription receives. Empty means every type. The vocabulary is the event socket’s (the computer’s events_url) less file.changed: window.opened, window.closed, window.focused, window.blurred, clipboard.changed, process.exited, computer.idle, computer.ready, computer.started, computer.stopped, computer.suspended.

computersalwaysstring[]

The computer ids this subscription receives events for. Empty means every computer in scope. Not checked against your computers when you set it — a subscription may name a computer you are about to create.

enabledalwaysboolean

Whether deliveries are made. Set false by the platform when an endpoint has failed for a day, or when the account moves to a plan without webhooks — see disabled_reason. Back to true by you with PATCH, which starts fresh, or, for plan, by the platform when you choose a plan again, which starts fresh too.

disabled_reasonalways"customer" | "failing" | "plan" | "null"

Why enabled is false: customer when you disabled it, failing when the platform did — a delivery ran out of attempts and nothing had been accepted for 24 hours, counted from the later of the last 2xx and when the subscription was last enabled — and plan when the account moved to a plan without webhooks. A plan subscription is enabled again, oldest first, as many as the plan allows, when the account chooses a plan that includes webhooks. null while enabled.

disabled_atalwaysstring or null

RFC 3339 timestamp, or null while enabled.

last_success_atalwaysstring or null

When the endpoint last answered 2xx to any delivery. null until it has.

last_failure_atalwaysstring or null

When a delivery attempt last failed. null until one has.

last_statusalwaysinteger or null

The HTTP status of the newest attempt, whatever it was. null before any attempt, or when the newest got no answer.

workspace_idstring

The workspace this subscription is confined to. Present only when it was created with a workspace-scoped API key; absent on an account-wide subscription.

created_atalwaysstring

RFC 3339 timestamp.

updated_atalwaysstring

RFC 3339 timestamp.

Failures

413

This route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.

DELETE/webhooks/{id}

Delete a webhookmember+

Removes the subscription and every delivery record it holds, pending ones included. Nothing more is sent to the endpoint.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X DELETE "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    result = client.webhooks.delete(id)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const result = await client.webhooks.delete(id);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "ok": true
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, PATCH, DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Response

Gone.

Response fields

okboolean

Always true. A failure is a status code, not ok: false.

POST/webhooks/{id}/rotate

Rotate a webhook secretmember+

Mints a new secret and answers it — once, like a create. The old one goes on being honoured for 24 hours: every delivery in that window carries two signatures on the one webhook-signature header, new first, separated by a space, and a verifier that accepts either passes throughout. Rotating again inside the window replaces the previous secret rather than keeping three.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8/rotate' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    result = client.webhooks.rotate(id)
    print("Secret returned once; store result.secret securely without logging it.")

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const result = await client.webhooks.rotate(id);
console.log("Secret returned once; store result.secret securely without logging it.");

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "whk-e1f2a3b4c5d6e7f8",
  "url": "https://example.com/mandala-events",
  "description": "Desktop readiness notifications",
  "events": [
    "computer.ready"
  ],
  "computers": [],
  "enabled": true,
  "disabled_reason": null,
  "disabled_at": null,
  "last_success_at": null,
  "last_failure_at": null,
  "last_status": null,
  "created_at": "2026-09-16T12:00:00.000Z",
  "updated_at": "2026-09-16T12:00:01.000Z",
  "secret": "whsec_Hx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQA="
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: POST, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Response

The subscription, with its new secret — shown once.

Response fields

idalwaysstring

whk- and sixteen hex characters.

urlalwaysstring

Where deliveries are POSTed.

descriptionalwaysstring
eventsalwaysstring[]

As on Webhook.

computersalwaysstring[]

As on Webhook.

enabledalwaysboolean
disabled_reasonalwaysstring or null
disabled_atalwaysstring or null
last_success_atalwaysstring or null
last_failure_atalwaysstring or null
last_statusalwaysinteger or null
workspace_idstring

As on Webhook: present only on a workspace-confined subscription.

created_atalwaysstring

RFC 3339 timestamp.

updated_atalwaysstring

RFC 3339 timestamp.

secretalwaysstring

The signing secret: whsec_ and 44 characters of base64. Shown here and never again — store it now. Every delivery is signed with it; see the resource description for how to verify.

POST/webhooks/{id}/test

Send a test deliverymember+

Queues one signed delivery of a synthetic event — {"type": "webhook.test", "computer": "", "source": "control-plane", "at": …, "data": {"subscription": …}} — through the ordinary path, so it is signed, retried and recorded exactly as a real one. The answer is the delivery record, accepted rather than finished: read what the endpoint said back from GET /webhooks/{id}/deliveries. A disabled subscription is a 409; enable it first. While the account’s plan includes no webhooks, a test is a 402.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8/test' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    result = client.webhooks.test(id)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const result = await client.webhooks.test(id);
console.log(JSON.stringify(result, null, 2));

202 Illustrative successful response.

HTTP 202
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "whd-f1a2b3c4d5e6f7a8",
  "event_type": "webhook.test",
  "computer": "",
  "cursor": "test:whd-f1a2b3c4d5e6f7a8",
  "state": "pending",
  "attempts": 0,
  "next_at": "2026-09-16T12:00:00.000Z",
  "attempted_at": null,
  "last_status": null,
  "last_error": null,
  "delivered_at": null,
  "created_at": "2026-09-16T12:00:00.000Z"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: POST, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Response

The queued delivery.

Response fields

idalwaysstring

whd- and sixteen hex characters: the webhook-id header this delivery carried, fixed across attempts.

event_typealwaysstring

The event’s type. gap for a gap frame; webhook.test for a test delivery.

computeralwaysstring

The computer the event is about. Empty on a test delivery.

cursoralwaysstring

The event’s own cursor — what to add as since= to the computer’s events_url to read on from it. Two kinds of delivery carry something else, which is not a socket position and must not be sent as since=: a test delivery’s is test: and its delivery id, and an event that arrived without a cursor of its own is keyed body: and a hash of its bytes.

statealways"pending" | "in_flight" | "delivered" | "exhausted" | "dropped"

pending (an attempt is scheduled), in_flight (one is running), delivered (a 2xx came back), exhausted (eight attempts failed) or dropped (the subscription was disabled or deleted first).

attemptsalwaysinteger

How many times it has been sent. Eight is the last.

next_atalwaysstring or null

When the next attempt is due, RFC 3339. null once the delivery is finished.

attempted_atalwaysstring or null

When the newest attempt started. null before the first.

last_statusalwaysinteger or null

The HTTP status of the newest attempt, or null when it got no answer.

last_erroralwaysstring or null

One line about the newest failure, for example timeout, dns, refused, reset, unreachable, tls, redirect, address refused or status NNN; other values are possible. A dropped delivery says why it was dropped. null after a success and before any attempt.

delivered_atalwaysstring or null

When the 2xx came back. null otherwise.

created_atalwaysstring

RFC 3339 timestamp: when the event reached the queue.

GET/webhooks/{id}/deliveries

List deliveriesviewer+

The newest hundred deliveries to this subscription, newest first, each with its state, its attempt count and the status or one-line error of its newest attempt. Finished deliveries are kept until seven days after they were queued; pending ones are kept until they finish. This is where an exhausted delivery shows up — nothing is dropped silently.

Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.

curl

base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/webhooks/whk-e1f2a3b4c5d6e7f8/deliveries' \
  -H "Authorization: Bearer $MANDALA_API_KEY"

Python — Mandala SDK 0.16.0

# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client

with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
    id = "whk-e1f2a3b4c5d6e7f8"
    result = client.webhooks.deliveries(id)
    print(result)

TypeScript — Mandala SDK 0.16.0

// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";

const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "whk-e1f2a3b4c5d6e7f8";
const result = await client.webhooks.deliveries(id);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

[
  {
    "id": "whd-f1a2b3c4d5e6f7a8",
    "event_type": "webhook.test",
    "computer": "",
    "cursor": "test:whd-f1a2b3c4d5e6f7a8",
    "state": "delivered",
    "attempts": 1,
    "next_at": null,
    "attempted_at": "2026-09-16T12:00:01.000Z",
    "last_status": 204,
    "last_error": null,
    "delivered_at": "2026-09-16T12:00:01.000Z",
    "created_at": "2026-09-16T12:00:00.000Z"
  }
]

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

405 Unsupported method on this public path; HEAD returns these headers without a body.

HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The webhook id.

Response

The deliveries, newest first.

Response fields

idalwaysstring

whd- and sixteen hex characters: the webhook-id header this delivery carried, fixed across attempts.

event_typealwaysstring

The event’s type. gap for a gap frame; webhook.test for a test delivery.

computeralwaysstring

The computer the event is about. Empty on a test delivery.

cursoralwaysstring

The event’s own cursor — what to add as since= to the computer’s events_url to read on from it. Two kinds of delivery carry something else, which is not a socket position and must not be sent as since=: a test delivery’s is test: and its delivery id, and an event that arrived without a cursor of its own is keyed body: and a hash of its bytes.

statealways"pending" | "in_flight" | "delivered" | "exhausted" | "dropped"

pending (an attempt is scheduled), in_flight (one is running), delivered (a 2xx came back), exhausted (eight attempts failed) or dropped (the subscription was disabled or deleted first).

attemptsalwaysinteger

How many times it has been sent. Eight is the last.

next_atalwaysstring or null

When the next attempt is due, RFC 3339. null once the delivery is finished.

attempted_atalwaysstring or null

When the newest attempt started. null before the first.

last_statusalwaysinteger or null

The HTTP status of the newest attempt, or null when it got no answer.

last_erroralwaysstring or null

One line about the newest failure, for example timeout, dns, refused, reset, unreachable, tls, redirect, address refused or status NNN; other values are possible. A dropped delivery says why it was dropped. null after a success and before any attempt.

delivered_atalwaysstring or null

When the 2xx came back. null otherwise.

created_atalwaysstring

RFC 3339 timestamp: when the event reached the queue.

Failures

403

A suspended account may still make this read: a 403 here is about the credential or the person holding it, never the account’s suspension.