Appearance
Webhooks
Webhooks push device and deployment activity to your own systems as it happens, so you do not have to poll the API. Subscribe an HTTPS endpoint to the event types you care about and Pantahub POSTs a signed JSON payload each time one occurs.
Typical uses: page an on-call channel when a rollout reports ERROR, kick off a CI job when a new revision lands, or mirror device metadata into your own inventory.
Creating a webhook
In the Hub, open Webhooks in the sidebar and choose Create webhook.
| Field | What it does |
|---|---|
| Name | A label for the subscription, e.g. production-fleet. |
| URL | Where deliveries are POSTed. Must be publicly reachable. |
| Description | Optional free text. |
| Event types | Which events to receive. Pick only what you handle. |
| Device filter | Optional, one device PRN per line. Empty means every device you own. |
| user-meta / device-meta keys | Optional. Narrows metadata events to changes touching those keys. |
| Custom headers | Optional Key: Value lines sent with every delivery — useful for routing or your own auth. |
| Enabled | Uncheck to pause deliveries without deleting the subscription. |
Save, and the subscription starts receiving events immediately. The list view shows each webhook's URL, selected event types, status and a row of actions: pause, send a test event, view deliveries, edit, delete.
Your signing secret is generated on creation. Store it somewhere safe — you need it to verify deliveries, and it is what proves a request came from Pantahub.
Filters
Device filter and metadata-key filters narrow which events reach you rather than what the payload contains. Leaving them empty is the right default; reach for them when a subscription is only interested in a subset of a large fleet, or in a handful of metadata keys that change far less often than the rest.
Event types
| Event | Fires when |
|---|---|
device.created | A device record is first observed. |
device.updated | Device fields changed (catch-all for non-metadata changes). Not emitted for the timemodified / meta-modified bookkeeping timestamps on their own. |
device.deleted | A device is marked as garbage. |
device.public.toggled | A device's public flag flipped. |
device.device_meta.updated | Device-reported metadata changed. |
device.user_meta.updated | User-set metadata (tags, labels) changed. |
step.created | A new trail step (revision) was pushed. |
step.progress.changed | A step's progress field changed. |
step.status.changed | A step transitioned between NEW, STARTED, DONE, UPDATED, WONTGO or ERROR. |
device.device_meta.updated is by far the highest-volume event, since devices report metadata continuously. Subscribe to it deliberately.
What a delivery looks like
http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Pantahub-Webhooks/1.0
Webhook-Id: del_ea133b51234ea0476701f51c
Webhook-Timestamp: 1785781088
Webhook-Signature: v1,5b5e884ffd67a58d…json
{
"id": "evt_0bc835e961bdd047351f6908",
"type": "step.status.changed",
"api_version": "2026-05-05",
"created": 1785781088,
"owner": "prn:::accounts:/696aa6c3c15c7b07794a09da",
"request_id": "del_ea133b51234ea0476701f51c",
"livemode": true,
"data": { "rev": 1, "status": "DONE", "prev_status": "NEW" }
}id identifies the event and is stable across retries — dedupe on it. request_id identifies the attempt and changes every time; it matches the Webhook-Id header.
Verifying a delivery
Your endpoint is a public URL, so treat every request as untrusted until the signature checks out. The signature is HMAC-SHA256(secret, "{Webhook-Id}.{Webhook-Timestamp}.{raw-body}"), hex-encoded.
python
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: bytes) -> bool:
wid = headers.get("Webhook-Id", "")
ts = headers.get("Webhook-Timestamp", "")
sig = headers.get("Webhook-Signature", "")
if not (wid and ts and sig):
return False
if abs(int(time.time()) - int(ts)) > 300: # reject stale replays
return False
expected = hmac.new(
secret, f"{wid}.{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
# Several signatures may be present during a rotation — any match wins.
return any(
v == "v1" and hmac.compare_digest(h, expected)
for v, _, h in (e.partition(",") for e in sig.split())
)Three things people get wrong:
- Read the raw body before parsing. Parsing JSON and re-serialising changes the bytes and the signature will not match.
- Accept any matching signature entry. During a secret rotation the header carries two, and code that only checks the first starts failing partway through the rotation.
- Never process an unsigned request. A missing signature is a rejection, not a fallback path.
For a stronger guarantee on high-value actions, re-fetch the event by its id and compare — a fabricated event returns 404:
sh
curl -s -H "Authorization: Bearer $TOKEN" \
https://api.pantahub.com/webhooks/events/evt_0bc835e961bdd047351f6908Answering a delivery
The status code you return is the whole protocol. Response bodies are recorded for debugging and otherwise ignored.
| You return | Result |
|---|---|
2xx | Success. Delivery complete. |
408, 425, 429, 5xx | Retried with exponential backoff. |
Any other 4xx | Permanent failure — dead-lettered, never retried. |
That last row catches people out: returning 400 on a payload you could not parse, or 403 from your own auth layer, discards the event for good. If the failure is transient on your side, return 503.
Deliveries must complete within 10 seconds. Validate, enqueue, return 2xx — do not do the real work inline, or you will start timing out under bursts and turn a slow moment into a retry storm.
Retries
Failed deliveries retry with exponential backoff — by default 8 attempts starting at 30 s, doubling each time:
60s → 120s → 240s → 480s → 960s → 1920s → 3840sRoughly two hours in total. After the attempts are exhausted, or immediately on a non-retryable status, the delivery is dead-lettered and stops retrying. It stays visible in the UI and can be replayed by hand.
Delivery guarantees
Delivery is at-least-once: if a delivery succeeds on your side but the acknowledgement is lost, it will be sent again. Make your handler idempotent and dedupe on the event id.
There is no ordering guarantee. Events are delivered concurrently and retries reorder relative to newer events, so do not assume step.created arrives before the step.status.changed that follows it. Use the created timestamp or the resource state in data where order matters.
Inspecting deliveries and events
Webhooks → (a webhook) → deliveries lists every attempt for that subscription: event, type, attempt number, status, HTTP response, timings, and a resend button per row.
Webhooks → Events is the account-wide log of everything the system emitted for you, whether or not a subscription matched. Select an event to see its full payload, which subscriptions it was delivered to, and the outcome of each. Filter by type, resource PRN or time range. Redeliver all fans a fresh attempt out to every matching subscription — useful after fixing a bug in your receiver.
Testing
Use the test action on a webhook to send a synthetic delivery without waiting for a real device event. It exercises the full path — signing, your endpoint, response handling — and shows up in the delivery log like any other attempt.
Rotating the signing secret
Use the rotate action on a webhook. Pantahub generates a fresh secret and keeps the previous one as a secondary, signing every delivery with both during the overlap. Update your endpoint to the new secret at your own pace; receivers checking every signature entry never see a failure. Rotate again later to drop the old secret.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| No deliveries at all | Subscription disabled, event type not selected, or a device filter excluding everything. |
| Signature never matches | Body parsed before hashing, or only the first signature entry checked. |
| Deliveries stop after one failure | You returned a non-retryable 4xx; check the delivery log for the recorded response. |
| Frequent timeouts | Work being done inline — enqueue and return instead. |
| Far more events than expected | Subscribed to device.device_meta.updated, which devices emit continuously. |
See also
- webhooks service — the REST API behind this page.
- Tokens — credentials for calling the API.