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 — thePublicWebhookEnvelope — 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:| Knob | What it does |
|---|---|
| Subject | partner, 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.) |
allowedOrgIds | Optional whitelist that narrows a partner/enterprise subscription to a subset of its orgs. Forbidden on org-subject subscriptions (redundant). |
eventTypes | The exact set of event types this endpoint receives. An event is delivered only if its type is in this list. |
categoryFilter | Optional list of categoryIds. When set, emergency events are delivered only when the emergency’s category is in the list. (Does not affect device.* events.) |
apiVersion | The payload version this subscription is pinned to (see Versioning). |
| Custom headers | Arbitrary request headers Rescue attaches to every delivery — used to authenticate the callback (see Securing your endpoint). |
orgId — it’s the org the event actually happened in, not the subscription’s subject.
The delivery envelope
Every webhook is an HTTPPOST with this top-level JSON body. Only data varies by event type.
| Field | Type | Notes |
|---|---|---|
eventId | string | Stable id for the underlying event, shared across all subscriptions that receive it. Your dedup key. |
eventType | string | One of the event types below. Switch on this. |
apiVersion | string | The payload version this delivery conforms to — the version pinned on your subscription. |
sequence | integer | Orders events for the same entity. See Ordering & idempotency. |
createdAt | string | ISO-8601 (UTC) timestamp the delivery was built. |
orgId | string | The org the event happened in. Route on this. |
subscriptionId | string | The subscription this delivery was sent to. |
deliveryId | string | Unique per delivery attempt — do not use it for dedup (retries get a new one). |
data | object | Event-type-specific payload. |
Event types
A subscription receives only the event types it was registered for. Today’s catalog:eventType | data shape | When it fires |
|---|---|---|
emergency.declared | emergency | A new emergency is created in a covered org. |
emergency.categorized | emergency | An emergency’s category changes (initial categorization or re-categorization). |
emergency.resolved | emergency | An emergency is resolved. |
device.offline | device | A device (base station, wearable, repeater, or card) crosses its offline threshold. Debounced — re-fires only after recovery. |
device.online | device | A previously-offline device starts reporting again. Carries offlineDurationMinutes. |
device.power_lost | device | A repeater reports it has lost mains power and is on battery. One-shot per episode; no paired recovery event. |
device.connection_switched | device | A 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.
| Field | Notes |
|---|---|
emergencyId | Rescue’s id for the incident. |
externalId | Your correlation id, if you set one on declare. May be null. |
status | declared, categorized, or resolved — tracks eventType. |
category | { id, name, color, severity }. color and severity may be null. |
source | The reporting source (e.g. verkada-camera), if known. |
description | Free-text description, if any. |
occurredAt | ISO-8601 time the incident occurred. |
location | { latitude, longitude, floorId } — any field may be null; whole object is null when unknown. |
declaredBy | Actor { type, id, name }. type is user, api_key, or system. API-key actors carry the rk_live_… key id. |
resolvedBy | Actor who resolved it — present only on emergency.resolved, null otherwise. |
Device data
Payload for the device.* events.
| Field | Notes |
|---|---|
deviceId | The device’s id. |
deviceType | base-station, wearable, repeater, or card. |
name | Human-friendly device name, if set. |
status | offline / online on the offline/online pair. null on device.power_lost and device.connection_switched — there the eventTypeis the signal. |
lastSeen | ISO-8601 time the device was last heard from. |
detectedAt | ISO-8601 time Rescue detected the transition. |
offlineDurationMinutes | Whole minutes the device was down. Set on device.online only. |
Delivery semantics
- Transport. HTTP
POSTto your subscription URL with the JSON envelope as the body andContent-Type: application/json. - Headers. Every request carries
Content-Type: application/jsonandUser-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
2xxmarks 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 response | What Rescue does |
|---|---|
2xx | Delivery recorded as succeeded. |
4xx (except 429) | Recorded as failed, not retried — treated as a permanent rejection of the payload. |
5xx, 429, network error, or timeout | Retried with backoff; exhausted attempts land in a dead-letter queue. |
Ordering & idempotency
- Dedup on
eventId. The sameeventIdis sent to every subscription for a given event and is your dedup key. Do not dedup ondeliveryId— that’s unique per attempt and changes on retry. - Order on
(entity, sequence). Deliveries can arrive out of order. Usesequenceto order events for the same entity:Event sequenceOrder 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—
eventId, drops duplicates, and never lets a lower sequence overwrite committed state for the same entity.
Securing your endpoint
- 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
orgIdis one you expect andeventTypeis 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 syntheticwebhook.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:
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
Ack fast, process later
Ack fast, process later
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.Make handlers idempotent
Make handlers idempotent
Delivery is at-least-once. Dedup on
eventId and design writes so re-processing the same event is a no-op.Order with sequence, not arrival time
Order with sequence, not arrival time
Never assume
declared arrives before resolved. Order on (emergencyId, sequence) / (deviceId, sequence) and ignore stale, lower-sequence updates.Tolerate growth
Tolerate growth
New event types and new
data fields can appear within your apiVersion. Switch on known eventTypes and ignore the rest instead of erroring.Keep your endpoint healthy
Keep your endpoint healthy
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.