Skip to main content

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
A phone beside a developer terminal in a quiet workspace, lit by one restrained red signal.
Deploys · now

Build passed

main@a1b2c3d deployed in 41s

One system event becomes a visible, attributable human moment.

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 URL

Keep live progress on the lock screen

Webhook + live_status

Secret URL

Receive every eligible room event

Outgoing webhook

HMAC

Build a room-based application

Human REST API

Bearer JWT

Connect an autonomous agent

Agent API / MCP

Scoped token

One 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.

quick-start.sh
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"}'
201 sentnotification_id: 0198f2c1…

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

  1. Producer

    CI · cron · agent

  2. Ingress

    one authenticated POST

  3. Room

    event + durable state

  4. Person

    push · feed · live card

  5. Resolution

    ack · answer · expiry

signed return path

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.

10%

running

Update

Merges sparse fields into the same card. This leg stays silent.

60%

done / failed

Resolve

Sends the terminal alert and ends the live card.

100%
A person glances at a phone while an automated job finishes in a quiet workspace.
Seven templates cover status, steps, progress, metrics, countdowns, questions, and matchups.
live-status.sh
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"}
}"
Read the full live-status contract

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.
verify-signature.mjs
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.
HTTPCauseRecovery
403invalid_secret / disabled / owner_not_proRe-copy, re-enable, or restore the room owner's plan.
410room_abandonedStop retrying; the owner account is gone.
429cooldown / rate limitHonor retry_after and retry once.
422invalid payloadCheck title length, object shape, and byte limits.

The same event model goes further.

Stop checking. Start knowing.

Create a room, copy its webhook URL, and turn the next system event into a Ping that lands.