Send the signal. Keep the state.
Send and receive PingRoom events from any external system. Fire a Ping, stream live progress to the lock screen, or consume the human result at your own endpoint.
https://api.pingroom.io/api
Build passed
main@a1b2c3d deployed in 41s
Pick the lane that matches the job.
Most integrations need only the first row. The other lanes use the same room and event model when you need state, a return path, or autonomous access.
Fire from CI, cron, a monitor, or a form
Incoming webhook
Secret URLKeep live progress on the lock screen
Webhook + live_status
Secret URLReceive every eligible room event
Outgoing webhook
HMACBuild a room-based application
Human REST API
Bearer JWTConnect an autonomous agent
Agent API / MCP
Scoped tokenOne URL. One POST. No SDK.
Create an incoming webhook in a room, copy its URL, and send JSON. An empty object is valid; every field can fall back to the webhook's saved defaults.
The URL is the credential. Keep it in a secret store, never in source control or logs.
GET is deliberately read-only, so link scanners and browser prefetch can never fire the room. OnlyPOST sends a Ping.
curl -X POST "https://api.pingroom.io/api/webhooks/{ROOM_CODE}/{SECRET}" \
-H 'Content-Type: application/json' \
-d '{"title":"Build passed","message":"main@a1b2c3d deployed in 41s"}'
The event loop stays connected.
Delivery is only the first half. Identity and state move with the event until a person acknowledges it, answers it, or lets it expire.
correlation_id → the shared spine
Producer
CI · cron · agent
Ingress
one authenticated POST
Room
event + durable state
Person
push · feed · live card
Resolution
ack · answer · expiry
The acknowledgement, answer, or expiry returns to your HTTPS endpoint.
- notification_id
- PingRoom's durable join key for feed and lifecycle events.
- correlation_id
- Your request ID across the Ping, stream, ack, and webhook.
- data
- Machine-readable context that survives every read surface.
- requires_ack
- Turns delivery into acknowledged or expired resolution.
- is_urgent
- Delivers time-sensitive, breaking through Focus. Independent of requires_ack — it changes delivery, not what is asked of the reader.
One card, updated in place.
Add live_status and a requiredcorrelation_id. The first leg alerts, intermediate legs update silently, and the terminal leg alerts once and closes the card.
running
Create
Starts one feed row and one live card. This leg alerts.
running
Update
Merges sparse fields into the same card. This leg stays silent.
done / failed
Resolve
Sends the terminal alert and ends the live card.

URL="https://api.pingroom.io/api/webhooks/{ROOM_CODE}/{SECRET}"
CID="deploy-$(date +%s)"
# Create the card and alert once.
curl -X POST "$URL" -H 'Content-Type: application/json' -d "{
"correlation_id": "$CID",
"title": "Deploying",
"live_status": {
"state": "running", "template": "progress",
"progress": 0.1, "message": "Installing dependencies"
}
}"
# Update the same card silently.
curl -X POST "$URL" -H 'Content-Type: application/json' -d "{
"correlation_id": "$CID",
"live_status": {
"state": "running", "progress": 0.6, "message": "Running checks"
}
}"
# Finish once and send one terminal alert.
curl -X POST "$URL" -H 'Content-Type: application/json' -d "{
"correlation_id": "$CID",
"live_status": {"state": "done", "progress": 1, "message": "Live in 41s"}
}"
The result comes back signed.
An outgoing webhook sends each eligible event to your HTTPS endpoint. Verify the signature against the raw request bytes, reject stale timestamps, and deduplicate retries with the delivery ID.
- Signature
- Verify V2 whenever it is present; never fall back to V1.
- Replay window
- Five minutes around the signed Unix timestamp.
- Delivery ID
- Stable across retries; store it before processing.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.PINGROOM_SIGNING_SECRET;
app.post(
"/hooks/pingroom",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body;
const timestamp = req.get("X-PingRoom-Timestamp") ?? "";
const deliveryId = req.get("X-PingRoom-Delivery") ?? "";
const signatureV2 = req.get("X-PingRoom-Signature-V2");
const signatureV1 = req.get("X-PingRoom-Signature") ?? "";
const hasV2 = signatureV2 !== undefined;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!secret || !Number.isFinite(age) || age > 300) {
return res.sendStatus(401);
}
// V1 is considered only when the V2 header is absent.
const prefix = hasV2
? `v2\n${timestamp}\n${deliveryId}\n`
: `${timestamp}.`;
const expected = crypto
.createHmac("sha256", secret)
.update(prefix)
.update(rawBody)
.digest("hex");
const received = hasV2 ? signatureV2 : signatureV1;
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody.toString("utf8"));
// Enqueue event and dedupe by deliveryId, then ACK fast.
return res.sendStatus(200);
},
);
Parse JSON only after verification. Re-serializing the object changes the signed bytes.
Small contract. Sharp guardrails.
These are the constraints worth putting into tests. Every 429 carries retry_after; idempotent retries replay the stored response instead of sending twice.
Never log an incoming webhook URL. Its secret lives in the path.
title40 charactersPut detail in message.message500 charactersHuman-readable notification body.data25 keys · 8 KiBMachine-readable object, never a JSON list.correlation_id255 charactersYour join key across the event loop.| HTTP | Cause | Recovery |
|---|---|---|
| 403 | invalid_secret / disabled / owner_not_pro | Re-copy, re-enable, or restore the room owner's plan. |
| 410 | room_abandoned | Stop retrying; the owner account is gone. |
| 429 | cooldown / rate limit | Honor retry_after and retry once. |
| 422 | invalid payload | Check title length, object shape, and byte limits. |
The same event model goes further.
Questions
Ask one person or a room for a predefined option or short text. Pending resolves once to answered, expired, or cancelled.
Agent API / MCP
Give an autonomous agent scoped, revocable access to rooms, questions, approvals, live status, and incoming events.
Human REST API
Build with rooms, quick actions, the feed, acknowledgement, and webhook management using the signed-in user's bearer token.
Stop checking. Start knowing.
Create a room, copy its webhook URL, and turn the next system event into a Ping that lands.