Skip to main content
Polling a run to find out when it finished works, and it is the wrong shape for anything that runs unattended. A webhook subscription turns those four moments into requests against an endpoint you own.
What it costs. Nothing. Subscriptions are not metered, and a delivery is not a step.What you need. An API key with devices:read, and an https endpoint reachable from the public internet. Your laptop behind a private address will be refused at subscribe time, not at delivery time; use a tunnel or a deployed endpoint.There is no console screen for this. Webhooks are API only.

Subscribe

201, and the response carries the signing secret once:
Store the secret now. To get another you have to rotate, and rotating stops the old one working. secretHint is its last four characters, which is there so you can tell which secret you configured without holding the secret to compare. An org may hold up to 20 subscriptions.

What the destination must be

The url is checked when you subscribe, and a refusal names which rule you hit: not_a_url, scheme_not_https, has_credentials, private_address, too_long. It must be https, must carry no credentials in it, must be at most 2000 characters, and must not be a private, loopback or link local address. That last one is not a style preference: this service would be the one making the request, so an address inside a private network is an instruction to probe somebody’s network from ours.

The four events

An unknown event name is refused rather than ignored, so a typo fails at subscribe time instead of producing a subscription that never fires. reason and detail on run.ended are present and null rather than absent when there is nothing to say. A receiver branching on why a run ended needs to tell “no reason” apart from “not told”.

The body

occurredAt is when the event happened, not when this attempt was made. data is deliberately shallow and carries ids rather than nested records. A receiver that needs the run reads GET /v1/runs/{agentRunId}, which is the one definition of what a run is. Embedding a copy here would publish a second one that drifts. Three headers come with it:

Verify the signature

The hex is HMAC-SHA256 of <t>.<raw body> under your secret.
Three things that are easy to get wrong:
  1. Compute over the raw bytes you received, before any JSON parsing. A body that has been through JSON.parse and back is not the body that was signed.
  2. Compare in constant time. A byte at a time comparison leaks the answer.
  3. Check t against your own clock. The timestamp is inside the signed material precisely so that a captured delivery cannot be replayed at you a year later. The reference verifier uses a 300 second tolerance.

Delivery is at least once

Plan for a duplicate. Deduplicate on Phonebase-Delivery.
  • A non 2xx answer is retried, 5 attempts in all, with gaps of 30 seconds, 2 minutes, 5 minutes and 18 minutes, so about twenty five minutes from first to last. Then it is given up on.
  • Each attempt gets 10 seconds before it times out.
  • Redirects are not followed. A 30x is recorded as the answer it is, because following one would send your payload to a url nobody checked.
  • Answer 2xx fast and do the work afterwards. An endpoint that does its processing before replying will be retried while it is still working.

Test it before you trust it

202 with { "deliveryId": "...", "status": "pending" }. It queues one delivery on the same path a real event takes: same signature, same headers, same retries. It answers as soon as the delivery is queued rather than waiting for your endpoint, so read the outcome from the deliveries listing. It arrives as run.ended with test: true in the data, because a test that came in as something your router does not recognise would exercise the transport and nothing else.

Read what happened

Newest first, limit between 1 and 100 and 50 by default, paged with the nextBefore cursor the response hands back. Each row says what was tried and what came of it: status, attempts, responseStatus, error, payloadPreview (the delivered body, truncated), lastAttemptAt and nextAttemptAt. Read status carefully. pending covers both never tried and waiting to retry. failed means every attempt is spent, not that your endpoint said no. responseStatus is null when the attempt never got one, and error then says why: a timeout, a refused connection, a name that did not resolve.
payloadPreview is the fastest way to see the exact shape of an event you have not handled before. Subscribe, fire one, read the preview.

Turning one off, and rotating its secret

Rotation has no overlap window. A receiver still verifying with the old secret will reject every delivery from that moment. Put the new secret in place first, or accept a gap.
Disable rather than delete when you want to keep the history: a delivery log for a subscription nobody can see is a log nobody can interpret, so deleting takes it too.

Scope and tenancy

Every webhook route needs devices:read and nothing else. There is no role gate: a member with that scope can manage subscriptions. A webhookId belonging to another org answers 404, the same 404 an id that names nothing gets. This is not an existence oracle.

Where to go next

Runs

What state, reason and detail on run.ended mean.