Webhooks

Create a webhook

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 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.

Requires the member role or higher.

post/webhooks

Request

  • Base URL: https://app.mandala.computer/api/v1
  • URL: https://app.mandala.computer/api/v1/webhooks
  • Auth: HTTP bearer

Request body

urlstring required

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.

eventsstring[]

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.

idstring required

whk- and sixteen hex characters.

urlstring required

Where deliveries are POSTed.

descriptionstring required
eventsstring[] required

As on Webhook.

computersstring[] required

As on Webhook.

enabledboolean required
disabled_reasonstring nullable required
disabled_atstring nullable required
last_success_atstring nullable required
last_failure_atstring nullable required
last_statusinteger nullable required
workspace_idstring

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

created_atstring required

RFC 3339 timestamp.

updated_atstring required

RFC 3339 timestamp.

secretstring required

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.

Changes

No recorded changes to this operation across all 1 revision of this API.