Descanto Docs
API Reference

Webhooks API

Endpoint CRUD, event types, HMAC signature verification, and the delivery log.

Webhook endpoints notify your own server about desktop lifecycle events, instead of you polling for them.

Event types

EventFired when
desktop.createdA new desktop finishes provisioning.
desktop.wokenA desktop's guest becomes responsive after a wake.
desktop.hibernatedA desktop finishes hibernating -- data.reason distinguishes a normal hibernate confirm from a host-failure-triggered one.
desktop.destroyedA desktop is permanently destroyed.

Every delivery's JSON payload carries at least:

{ "type": "desktop.woken", "desktopId": "desk_..." }

List webhook endpoints

GET /v1/webhooks

curl -sS "$BASE/webhooks" -H "$AUTH"
{
  "webhooks": [
    {
      "id": "wh_...",
      "url": "https://example.com/hooks/canto",
      "events": ["desktop.woken", "desktop.hibernated"],
      "createdAt": "2026-08-20T12:00:00Z"
    }
  ]
}

Endpoints belonging to the caller's org -- never includes secret.

StatusMeaning
200The org's webhook endpoints
401Missing/invalid credentials

Create a webhook endpoint

POST /v1/webhooks

Request body (CreateWebhookBody):

{ "url": "https://example.com/hooks/canto", "events": ["desktop.woken"] }
FieldTypeRequiredDescription
urlstringyesAbsolute https:// URL, at most 2000 chars, no userinfo (user:pass@). Loopback and link-local hosts (localhost, 127.0.0.0/8, ::1, 0.0.0.0/::, 169.254.0.0/16, fe80::/10) are rejected in either scheme, except on deployments that opt in to allowing them for local dev.
eventsstring[]yesNon-empty; one or more of the event types above; deduped server-side.
curl -sS -X POST "$BASE/webhooks" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com/hooks/canto", "events": ["desktop.woken", "desktop.hibernated"]}'

Response (CreateWebhookResponse, 201) -- the only response that ever carries secret:

{
  "id": "wh_...",
  "url": "https://example.com/hooks/canto",
  "events": ["desktop.woken", "desktop.hibernated"],
  "secret": "canto_whsec_...",
  "createdAt": "2026-08-20T12:00:00Z"
}

Store secret -- it's shown exactly once. There's no rotation endpoint yet; rotating means delete + recreate.

StatusMeaning
201Webhook endpoint created -- secret shown exactly once
400Invalid/non-https URL, a loopback or link-local URL host, userinfo in the URL, URL over 2000 chars, empty/unknown events, the per-org endpoint cap reached, or a malformed body
401Missing/invalid credentials

Delete a webhook endpoint

DELETE /v1/webhooks/{id}

curl -sS -X DELETE "$BASE/webhooks/$WEBHOOK_ID" -H "$AUTH"

Deleting an endpoint cascades its delivery log; any still-pending deliveries silently stop.

StatusMeaning
204Endpoint deleted
401Missing/invalid credentials
404No such webhook in the caller's org (also returned for a wrong-org id -- never distinguishable from absent)

List an endpoint's deliveries

GET /v1/webhooks/{id}/deliveries

ParamInRequiredDescription
idpathyesThe webhook endpoint id
limitquerynoMax deliveries to return. Default 50, max 200.
curl -sS "$BASE/webhooks/$WEBHOOK_ID/deliveries?limit=20" -H "$AUTH"
{
  "deliveries": [
    {
      "id": "whd_...",
      "eventType": "desktop.woken",
      "state": "delivered",
      "attempts": 1,
      "lastStatus": 200,
      "lastError": null,
      "nextAttemptAt": null,
      "payload": { "type": "desktop.woken", "desktopId": "desk_..." },
      "createdAt": "2026-08-20T12:00:00Z",
      "deliveredAt": "2026-08-20T12:00:01Z"
    }
  ]
}

Newest first. state is pending | delivered | failed. nextAttemptAt is null once a delivery has settled. payload is the exact JSON body that is/was POSTed to your endpoint.

StatusMeaning
200The endpoint's delivery log, newest first
400Non-numeric or out-of-range limit
401Missing/invalid credentials
404No such webhook in the caller's org

Verifying deliveries

Every delivery carries:

Canto-Webhook-Id: whd_...
Canto-Webhook-Event: desktop.woken
Canto-Signature: t=1755691200,v1=<hex HMAC-SHA256>

Canto-Signature is t=<unix-secs>,v1=<hex HMAC-SHA256(secret, "{t}.{raw_body}")>. Verify it over the raw request body (before any JSON parsing), with a constant-time comparison and a freshness check against t (a 300s tolerance is the SDK helpers' default):

# conceptually:
expected = hex(HMAC_SHA256(secret, f"{t}.{raw_body}"))

Both the TypeScript and Python SDKs ship a verifyWebhookSignature / verify_webhook_signature helper that does this correctly (constant-time compare, malformed-header handling, no exceptions on a forged header) -- prefer it over reimplementing the check.

On this page