Webhooks

PingRoom rooms have two webhook connectors. Incoming webhooks let any system fire a ping into a room. Outgoing webhooks forward every ping back out to a URL you control — signed, retried, and delivered at the edge. Signals in, actions out.

Input · Free

Incoming webhooks

An incoming webhook is a single URL that fires a room when anything POSTs to it. A room can carry up to four, one per quick action — CI, a cron job, a form backend, an IoT button, whatever can make an HTTP request. Incoming webhooks are free on every account.

Fire endpoint (public — no auth header):

GET | POST  /api/webhooks/{inviteCode}/{secret}

The {secret} in the path is the credential — anything that captures the URL can fire the webhook, so treat it like a password. PingRoom returns webhook URLs only to the room owner and marks every such response no-store. A GET works for systems that can only open a link; a POST with a JSON body unlocks the fields below.

Request body

Every field is optional — an empty POST still fires the room’s default action.

FieldTypeMeaning
titlestring ≤ 40Notification title. Overrides the webhook's saved title for this call.
messagestring ≤ 500Notification body. Overrides the webhook's saved message for this call.
actionint 1–4Which quick action to fire. Defaults to the webhook's configured action number.
dataobjectFree-form key/value bag carried alongside the ping. Optional.

Example:

# Fire a room. The secret in the URL is the credential — no auth header.
curl -sX POST https://api.pingroom.io/api/webhooks/ABC123/whk_9f3c…e1 \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: build-4821' \
  -d '{
    "title": "Deploy finished",
    "message": "Production is live ✅",
    "action": 1,
    "data": { "commit": "a1b2c3d", "env": "prod" }
  }'

A successful fire returns 200:

{
  "success": true,
  "message": "Webhook triggered successfully",
  "notification_id": "019e79be-3acd-73b6-b440-8ab0a7bffed8",
  "room": { "id": "…", "name": "Build Alerts" },
  "webhook": { "id": "…", "name": "CI", "emoji": "🚀" },
  "action": { "number": 1, "label": "Deploy done", "icon": "🚀" },
  "notification_title": "Deploy finished",
  "notification_message": "Production is live ✅",
  "triggered_at": "2026-06-02T18:24:05+00:00"
}

Idempotency & cooldown

  • Idempotency. Pass an Idempotency-Key header and a retried delivery replays the original response instead of firing a second ping. Keys are remembered for 1 hour.
  • Cooldown. Each webhook has a per-webhook cooldown (default and max 5s). Firing inside the window returns 429 cooldown_active with a retry_after in seconds. Cooldown is checked before idempotency, so a reused key cannot bypass it.
  • Rate limit. Each room+secret is capped at 60 calls per minute on top of the cooldown.

Incoming errors

Failures carry a stable error field — branch on the HTTP status and error, not the human message.

HTTPerrorMeaning
404room_not_foundNo active room matches the invite code in the URL.
403invalid_secretThe secret in the URL does not match any webhook on the room.
403webhook_disabledThe webhook exists but is currently switched off.
429cooldown_activeFired again inside the cooldown window — honor retry_after (seconds).
429(rate limit)More than 60 calls/min for this room+secret. Standard Retry-After header.
422(validation)A field failed validation — e.g. title over 40 chars or action outside 1–4.

Managing incoming webhooks

Owner-only, authenticated with the account JWT (Authorization: Bearer …). The fire endpoint above needs no auth; these management endpoints do.

MethodPathDoes
GET/api/rooms/{inviteCode}/webhooksList a room's webhooks (each with its full webhook_url).
POST/api/rooms/{inviteCode}/webhooksCreate a webhook. Auto-assigns the next free action number (1–4).
GET/api/rooms/{inviteCode}/webhooks/{id}Read one webhook.
PUT/api/rooms/{inviteCode}/webhooks/{id}Update label, title, message, emoji, sound, cooldown, enabled.
DELETE/api/rooms/{inviteCode}/webhooks/{id}Delete a webhook.
POST/api/rooms/{inviteCode}/webhooks/{id}/testSend a test fire to verify wiring.

Output · Pro

Outgoing webhooks

Point a room at a URL and PingRoom calls it every time the room pings — Slack, a dashboard, a logging pipeline, anything that receives a POST. One outgoing webhook per room. It fires independently of push recipients, so it still delivers even when no one is around to be pinged. Outgoing webhooks are a Pro feature; configuring one on a free account returns 402 pro_required.

Delivery runs through a Cloudflare Worker — globally distributed and low-latency. Deliveries are queued and retried up to 3 times with 10s / 30s / 60s backoff on transient failures. After 15 consecutive failures the webhook auto-disables so a dead endpoint stops burning invocations.

The event you receive

A compact, signal-only JSON body — no tokens, no PII beyond the sender’s display name.

FieldTypeMeaning
eventstringAlways "ping" today.
roomobject{ name, code } — the room the ping fired in.
titlestringThe ping title (custom title if one was set, else the room name).
bodystringSender name + message, e.g. "Mia: Dinner's ready".
senderstringDisplay name of whoever (or whatever) fired the ping.
action_numberint | nullWhich quick action fired, if any.
trigger_sourcestringHow the ping was raised: "manual", "webhook", "location", "time", "agent", "test".
timestampISO-8601When the ping fired.

What lands at your URL:

POST <your URL>
X-PingRoom-Signature: 3a7f… (lower-case hex HMAC-SHA256)
X-PingRoom-Timestamp: 1780000000
Content-Type: application/json

{
  "event": "ping",
  "room": { "name": "Build Alerts", "code": "ABC123" },
  "title": "Deploy finished",
  "body": "CI: Production is live ✅",
  "sender": "CI",
  "action_number": 1,
  "trigger_source": "webhook",
  "timestamp": "2026-06-02T18:24:05+00:00"
}

Verifying the signature

Every delivery is signed with the room’s signing_secret (read it from the config endpoint; rotate it with regenerate_secret). Recompute the HMAC and reject anything that doesn’t match — or whose timestamp is stale.

  • Signed string is {timestamp}.{rawBody} — the X-PingRoom-Timestamp value, a literal dot, then the exact request body bytes.
  • Signature is HMAC-SHA256(signing_secret, signedString), lower-case hex, sent in X-PingRoom-Signature.
  • Reject any request whose X-PingRoom-Timestamp is more than ±300s from now — that window is what makes a captured request un-replayable.
  • Compare in constant time, and verify the raw bytes before any JSON re-parse.
import crypto from "node:crypto";

// Express-style receiver. You MUST read the raw body bytes — verify the exact
// bytes that were signed, before any JSON re-serialization.
function verify(req, signingSecret) {
  const sig = req.header("X-PingRoom-Signature");
  const ts = req.header("X-PingRoom-Timestamp");
  if (!sig || !ts) return false;

  // Replay protection: reject anything older than ±300s.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const signed = `${ts}.${req.rawBody}`;           // "{timestamp}.{rawBody}"
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(signed)
    .digest("hex");

  // Constant-time compare.
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Target URL rules (SSRF safety)

  • HTTPS only, port 443 only, max 2048 chars. No embedded credentials (https://user:pass@host).
  • The host must resolve to a public IP. Private, loopback, link-local, and reserved ranges are rejected — including obfuscated literals (decimal, octal, hex, IPv4-mapped IPv6).
  • The Worker re-resolves and egress-filters at delivery time too, so a host that flips to an internal address after registration still can’t be reached.

Managing the outgoing webhook

Owner-only, authenticated with the account JWT. The config response includes the signing secret and is marked no-store.

MethodPathDoes
GET/api/rooms/{inviteCode}/outgoing-webhookRead the room's outgoing webhook config + signing_secret.
PUT/api/rooms/{inviteCode}/outgoing-webhookSet url / enabled; pass regenerate_secret to rotate the signing secret.
POST/api/rooms/{inviteCode}/outgoing-webhook/testSend a synchronous test delivery to the configured URL.

See also

For the bigger picture of how rooms wire to the outside world, see Connections. To let an AI agent fire rooms on your behalf, see Agent Access.