Skip to content

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.

FieldWhat it does
NameA label for the subscription, e.g. production-fleet.
URLWhere deliveries are POSTed. Must be publicly reachable.
DescriptionOptional free text.
Event typesWhich events to receive. Pick only what you handle.
Device filterOptional, one device PRN per line. Empty means every device you own.
user-meta / device-meta keysOptional. Narrows metadata events to changes touching those keys.
Custom headersOptional Key: Value lines sent with every delivery — useful for routing or your own auth.
EnabledUncheck 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

EventFires when
device.createdA device record is first observed.
device.updatedDevice fields changed (catch-all for non-metadata changes). Not emitted for the timemodified / meta-modified bookkeeping timestamps on their own.
device.deletedA device is marked as garbage.
device.public.toggledA device's public flag flipped.
device.device_meta.updatedDevice-reported metadata changed.
device.user_meta.updatedUser-set metadata (tags, labels) changed.
step.createdA new trail step (revision) was pushed.
step.progress.changedA step's progress field changed.
step.status.changedA 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_0bc835e961bdd047351f6908

Answering a delivery

The status code you return is the whole protocol. Response bodies are recorded for debugging and otherwise ignored.

You returnResult
2xxSuccess. Delivery complete.
408, 425, 429, 5xxRetried with exponential backoff.
Any other 4xxPermanent 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 → 3840s

Roughly 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

SymptomLikely cause
No deliveries at allSubscription disabled, event type not selected, or a device filter excluding everything.
Signature never matchesBody parsed before hashing, or only the first signature entry checked.
Deliveries stop after one failureYou returned a non-retryable 4xx; check the delivery log for the recorded response.
Frequent timeoutsWork being done inline — enqueue and return instead.
Far more events than expectedSubscribed to device.device_meta.updated, which devices emit continuously.

See also