Skip to main content

Overview

Webhooks push every Punch Rescue event to an HTTP endpoint you control, in real time. The moment an emergency is declared, re-categorized, or resolved — or a device goes offline, comes back, loses power, or fails over — Rescue fans the event out to the base-station devices in the org and to every registered webhook subscriber whose scope covers that org. Every delivery uses the same stable JSON envelope — the PublicWebhookEnvelope — regardless of event type, so you write one parser and switch on eventType.
Webhook subscriptions are registered by a Punch Rescue Admin portal, not through the Public API — there’s no public CRUD for subscriptions in v1. Tell your Punch Rescue contact the URL to call, which events you want, and the partner / enterprise / org scope, and they’ll provision it. Everything on this page describes what you’ll receive once a subscription exists.

How a subscription is scoped

A single subscription can cover one org or thousands. Your Rescue admin configures these knobs at registration:
KnobWhat it does
Subjectpartner, enterprise, or org — the subscription receives events for every org under that subject. (A reserved global subject covering all orgs exists for Rescue-internal use.)
allowedOrgIdsOptional whitelist that narrows a partner/enterprise subscription to a subset of its orgs. Forbidden on org-subject subscriptions (redundant).
eventTypesThe exact set of event types this endpoint receives. An event is delivered only if its type is in this list.
categoryFilterOptional list of categoryIds. When set, emergency events are delivered only when the emergency’s category is in the list. (Does not affect device.* events.)
apiVersionThe payload version this subscription is pinned to (see Versioning).
Custom headersArbitrary request headers Rescue attaches to every delivery — used to authenticate the callback (see Securing your endpoint).
Because the subject can span many orgs, always route on the envelope’s orgId — it’s the org the event actually happened in, not the subscription’s subject.

The delivery envelope

Every webhook is an HTTP POST with this top-level JSON body. Only data varies by event type.
{
  "eventId": "01HXYZABCDEFGHJKMNPQRSTVWX",
  "eventType": "emergency.declared",
  "apiVersion": "2026-05-01",
  "sequence": 1,
  "createdAt": "2026-05-12T14:32:11Z",
  "orgId": "8b0c1234-0000-0000-0000-000000000000",
  "subscriptionId": "01HXSUB0000000000000000000",
  "deliveryId": "01HXDEL0000000000000000000",
  "data": { "...": "event-type-specific; see below" }
}
FieldTypeNotes
eventIdstringStable id for the underlying event, shared across all subscriptions that receive it. Your dedup key.
eventTypestringOne of the event types below. Switch on this.
apiVersionstringThe payload version this delivery conforms to — the version pinned on your subscription.
sequenceintegerOrders events for the same entity. See Ordering & idempotency.
createdAtstringISO-8601 (UTC) timestamp the delivery was built.
orgIdstringThe org the event happened in. Route on this.
subscriptionIdstringThe subscription this delivery was sent to.
deliveryIdstringUnique per delivery attempt — do not use it for dedup (retries get a new one).
dataobjectEvent-type-specific payload.

Event types

A subscription receives only the event types it was registered for. Today’s catalog:
eventTypedata shapeWhen it fires
emergency.declaredemergencyA new emergency is created in a covered org.
emergency.categorizedemergencyAn emergency’s category changes (initial categorization or re-categorization).
emergency.resolvedemergencyAn emergency is resolved.
device.offlinedeviceA device (base station, wearable, repeater, or card) crosses its offline threshold. Debounced — re-fires only after recovery.
device.onlinedeviceA previously-offline device starts reporting again. Carries offlineDurationMinutes.
device.power_lostdeviceA repeater reports it has lost mains power and is on battery. One-shot per episode; no paired recovery event.
device.connection_switcheddeviceA base station fails over from Ethernet to cellular. One-shot per episode; no paired recovery event.
webhook.ping{ "message": "ping" }Synthetic test event — fired only on demand (see Testing your endpoint). Never fires automatically.
The catalog grows additively. New event types can appear within your apiVersion — ignore any eventType you don’t recognize rather than erroring.

Emergency data

Payload for the emergency.* events.
{
  "emergencyId": "01HXEMG0000000000000000000",
  "externalId": "incident-7421",
  "status": "declared",
  "category": { "id": "cat-1", "name": "Medical", "color": "red", "severity": 2 },
  "source": "verkada-camera",
  "description": "Detected at Gate 3",
  "occurredAt": "2026-05-12T14:32:10Z",
  "location": { "latitude": 41.88, "longitude": -87.63, "floorId": "floor-2" },
  "declaredBy": { "type": "api_key", "id": "rk_live_1a2b3c4d", "name": "Verkada bridge" },
  "resolvedBy": null
}
FieldNotes
emergencyIdRescue’s id for the incident.
externalIdYour correlation id, if you set one on declare. May be null.
statusdeclared, categorized, or resolved — tracks eventType.
category{ id, name, color, severity }. color and severity may be null.
sourceThe reporting source (e.g. verkada-camera), if known.
descriptionFree-text description, if any.
occurredAtISO-8601 time the incident occurred.
location{ latitude, longitude, floorId } — any field may be null; whole object is null when unknown.
declaredByActor { type, id, name }. type is user, api_key, or system. API-key actors carry the rk_live_… key id.
resolvedByActor who resolved it — present only on emergency.resolved, null otherwise.

Device data

Payload for the device.* events.
{
  "deviceId": "base-001",
  "deviceType": "base-station",
  "name": "Gym Base Station",
  "status": "online",
  "lastSeen": "2026-05-12T13:20:00Z",
  "detectedAt": "2026-05-12T14:32:11Z",
  "offlineDurationMinutes": 72
}
FieldNotes
deviceIdThe device’s id.
deviceTypebase-station, wearable, repeater, or card.
nameHuman-friendly device name, if set.
statusoffline / online on the offline/online pair. null on device.power_lost and device.connection_switched — there the eventTypeis the signal.
lastSeenISO-8601 time the device was last heard from.
detectedAtISO-8601 time Rescue detected the transition.
offlineDurationMinutesWhole minutes the device was down. Set on device.online only.

Delivery semantics

  • Transport. HTTP POST to your subscription URL with the JSON envelope as the body and Content-Type: application/json.
  • Headers. Every request carries Content-Type: application/json and User-Agent: Rescue-Webhook/1.0, plus any custom headers configured on your subscription. (Custom headers can’t override those two reserved names.)
  • Timeout. Rescue waits up to 10 seconds for your response. Respond fast — do the real work asynchronously after acking.
  • Success. Any 2xx marks the delivery succeeded.
  • At-least-once. Fan-out is asynchronous and retried, so you may receive the same event more than once. Make your handler idempotent (see below).

Retries & auto-disable

Your responseWhat Rescue does
2xxDelivery recorded as succeeded.
4xx (except 429)Recorded as failed, not retried — treated as a permanent rejection of the payload.
5xx, 429, network error, or timeoutRetried with backoff; exhausted attempts land in a dead-letter queue.
After 10 consecutive failed deliveries, Rescue auto-disables the subscription (status flips to disabled_failure) and stops sending. A successful delivery resets the counter. Re-enabling a disabled subscription is a Rescue-admin action — reach out to your contact once your endpoint is healthy again.

Ordering & idempotency

  • Dedup on eventId. The same eventId is sent to every subscription for a given event and is your dedup key. Do not dedup on deliveryId — that’s unique per attempt and changes on retry.
  • Order on (entity, sequence). Deliveries can arrive out of order. Use sequence to order events for the same entity:
    EventsequenceOrder by
    emergency.declared1(emergencyId, sequence)
    emergency.categorized2(emergencyId, sequence)
    emergency.resolved3(emergencyId, sequence)
    device.offline1(deviceId, sequence)
    device.online2(deviceId, sequence)
    device.power_lost / device.connection_switched1one-shot; no paired event
    webhook.ping0
A robust consumer keys on eventId, drops duplicates, and never lets a lower sequence overwrite committed state for the same entity.

Securing your endpoint

v1 does not sign deliveries with an HMAC signature. Authenticate callbacks using the custom headers on your subscription. HMAC request signing is on the roadmap as an opt-in enhancement in a future apiVersion.
  • Shared-secret header. Ask your Rescue admin to attach a secret header to your subscription (e.g. X-Webhook-Token: <random>) and reject any request that doesn’t carry the expected value. Header values are write-only — after registration only the header names are visible, so a leaked subscription record can’t reveal the secret.
  • HTTPS only. Serve the endpoint over TLS so the secret header and payload aren’t sent in the clear.
  • Validate what you receive. Confirm orgId is one you expect and eventType is one you handle before acting on a delivery.
  • Don’t trust the network alone. Combine the secret header with allow-listing Rescue’s egress where your infrastructure supports it.

Testing your endpoint

Rescue can fire a synthetic webhook.ping at your subscription so you can confirm reachability, auth headers, and parsing before any real event flows. Ask your Rescue admin to trigger a ping (for partner/enterprise subscriptions they pick which orgId the synthetic envelope carries). You’ll receive a standard envelope:
{
  "eventId": "01HXPING000000000000000000",
  "eventType": "webhook.ping",
  "apiVersion": "2026-05-01",
  "sequence": 0,
  "createdAt": "2026-05-12T14:32:11Z",
  "orgId": "8b0c1234-0000-0000-0000-000000000000",
  "subscriptionId": "01HXSUB0000000000000000000",
  "deliveryId": "01HXDEL0000000000000000000",
  "data": { "message": "ping" }
}
Return any 2xx and you’re wired up.

Versioning

apiVersion is date-versioned (YYYY-MM-DD) and pinned at subscription creation. The current version is 2026-05-01.
  • Additive changes ship within a version — new fields and new event types can appear without a version bump. Build tolerantly: ignore unknown fields and unknown eventTypes.
  • Breaking changes ship a new apiVersion. Old versions keep flowing to subscriptions pinned to them until you migrate, so an upgrade is never forced on you mid-integration.

Best practices

Return 2xx within the 10-second window, then do the heavy lifting (DB writes, downstream calls) asynchronously. Slow handlers get treated as timeouts and retried.
Delivery is at-least-once. Dedup on eventId and design writes so re-processing the same event is a no-op.
Never assume declared arrives before resolved. Order on (emergencyId, sequence) / (deviceId, sequence) and ignore stale, lower-sequence updates.
New event types and new data fields can appear within your apiVersion. Switch on known eventTypes and ignore the rest instead of erroring.
Ten consecutive failures auto-disable the subscription. Monitor your 2xx rate and return 5xx (not 4xx) for transient problems so Rescue retries instead of giving up.
Debugging a missed or malformed delivery? Reach out to your Rescue contact with the eventId, deliveryId, and subscriptionId — that triple lets us trace exactly what we sent and when.