PingRoom

人々に実用的なプッシュ通知を送り、質問をし、帰属可能な回答を受け取り、スコープ付きOAuth/MCPを介して人間に作業を引き継ぎます。

ドキュメント

PingRoom Agent API & MCP

Connect Claude, Cursor, a terminal tool, or your own SDK client to PingRoom. Hosted MCP uses OAuth 2.1 in the browser; the published CLI and SDK both pair through the PingRoom app. Lower-level clients can use the auth.md protocol. Every connection is a separate, revocable robot identity. A person claims it and delegates room access; the robot acts for them without replacing their PingRoom profile.

Choose a connection path

Use the hosted MCP endpoint for Claude or Cursor, or app pairing for the CLI and the SDK. Each path ends with a claimed, revocable robot and an explicit room grant; none asks you to paste an API token.

CLI: pair in the PingRoom app

Run the CLI, scan or open its link, verify the robot profile, choose its home room and room reach, then claim it. The saved connection is used by later commands and hooks. The current published CLI includes the automatic onboarding Question and the pingroom activate retry.

npm install -g @pingroom/cli
pingroom
# Scan the QR code or open the link, then choose a room and approve in PingRoom

pingroom ping -m "CLI connected"

Use Claude Code for long-running work? PingRoom is preparing up to ten seats in a 14-day, founder-supported cohort. Recruitment starts only after every release gate passes: the CLI is public and clean-installed, the server path and hosted MCP tools are deployed, a receipt-capable iPhone build is public, and a real-device receipt-to-answer-to-agent-observation test passes before the rollout flag is enabled. See the planned cohort.

MCP: authorize in the browser

Claude Code needs one add command. Run /mcp, select PingRoom, and choose Authenticate to complete OAuth. Cursor and Claude connectors use the same endpoint. Call activate_agent_inbox, then poll wait_for_handoff only while the result is pending. Report ready only for an answered result with activation_completed: true; stop at any other terminal result or a bounded local deadline.

claude mcp add --transport http pingroom https://api.pingroom.io/api/agent/mcp

Client-by-client MCP setup

SDK: app-approved pairing

The published @pingroom/sdk package registers a pending robot with its own profile, waits for its owner to claim it in PingRoom, adopts the active credential, and can explicitly run the time-bounded Agent Inbox activation check. The server, not the client, owns the complete agent-feature grant.

import { PingRoom } from '@pingroom/sdk';

const pingroom = new PingRoom();
const pairing = await pingroom.auth.startPairing({
  agent_label: 'Deploy bot',
});

console.log('Robot to claim:', pairing.agent?.profile ?? pairing.agent?.label);
console.log('Open in PingRoom:', pairing.pair_url);
const connection = await pingroom.auth.waitForPairing(pairing);
const activation = await pingroom.inbox.activate({
  overallTimeoutMs: 120_000,
});

console.log('Agent Inbox ready:', activation.activation_completed);

const homeRoom = connection.home_room ?? connection.room;
await pingroom.broadcast(homeRoom.invite_code, {
  message: 'SDK connected',
});

Discovery

The machine-readable description of how to register lives at pingroom.io/auth.md. An agent that hits a protected endpoint without a credential gets a 401 with a WWW-Authenticate header pointing to protected-resource metadata; that document names the authorization server:

Registering

Registration creates a separate agent identity. A person must bind it to their account and delegate access; the agent cannot claim itself. There is always proof of a real human in the chain. PingRoom supports three flows at POST /api/agent/auth:

  • Verified (ID-JAG): the agent presents a token signed by a trusted identity provider, audience-scoped to PingRoom. Verified against the provider’s public keys; an active credential is issued synchronously.
  • Verified email: the agent presents a provider token proving the user’s email. If it matches an existing account, an active credential is issued.
  • Anonymous + claim: the agent receives a short-lived, scope-less pre-claim credential and the user completes a one-time email code to bind it.

The credential is a bearer token presented as Authorization: Bearer <credential> on every request. A user can see and revoke connected agents from the app’s Connected Agents screen at any time.

Raw REST registration

This lower-level flow is for custom clients that manage credentials themselves. The CLI and the SDK both use app pairing, and MCP hosts should use OAuth. For verified REST flows, replace steps 1 to 3 with a single POST /api/agent/auth carrying your provider assertion, and you get an active credential back immediately.

# 1. Register (anonymous). Returns a short-lived pre-claim credential
curl -sX POST https://api.pingroom.io/api/agent/auth \
  -H 'Content-Type: application/json' \
  -d '{"type":"anonymous","scopes":["pingroom:rooms:write","pingroom:actions:trigger","pingroom:profile:write","pingroom:handoffs:create"],"agent_label":"My Agent"}'
# → { "credential": "<pre-claim JWT>", "credential_type": "pre_claim", "expires_in": 900, "claim": {...} }

PRECLAIM="<pre-claim JWT>"

# 2. Start the claim. Emails the user a one-time code
curl -sX POST https://api.pingroom.io/api/agent/auth/claim/start \
  -H "Authorization: Bearer $PRECLAIM" -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com"}'

# 3. Complete the claim with the code the user reads back. Returns the ACTIVE credential
curl -sX POST https://api.pingroom.io/api/agent/auth/claim/complete \
  -H "Authorization: Bearer $PRECLAIM" -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","otp":"123456"}'
# → { "credential": "<active JWT>", "credential_type": "active", "expires_in": null }

TOKEN="<active JWT>"

# 4a. Set a bot avatar
curl -sX POST https://api.pingroom.io/api/agent/profile/avatar \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"avatar_id":"bots-3"}'

# 4b. Create a room (free accounts: up to five rooms)
curl -sX POST https://api.pingroom.io/api/agent/rooms \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Build Alerts","icon":"bell","color":"#e33122"}'

# 4c. Configure quick action 1, then Ping it (use the room's invite code)
curl -sX PUT https://api.pingroom.io/api/agent/rooms/ABC123/actions/1 \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"label":"Deploy done","icon":"🚀","sound":"ting"}'

curl -sX POST https://api.pingroom.io/api/agent/rooms/ABC123/actions/1/trigger \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"trigger_source":"manual"}'

What agents can do

Each capability is gated by a scope on the credential. The agent remains visibly separate while acting on behalf of its owner, and every operation still obeys that owner’s permissions, plan, and the rooms explicitly delegated to the agent.

Create a room

Agents can create rooms they own. Free accounts may own up to five rooms; going past that returns 402 room_limit_reached. Pro lifts the limit. icon takes an id from PingRoom's room-icon catalog, not an emoji.

POST /api/agent/rooms · scope pingroom:rooms:write

{ "name": "Build Alerts", "icon": "bell", "color": "#e33122" }

Redeem a gifted or promotional Pro code

Apply a code to the human account that connected this agent. No room or existing Pro plan is required. The response reports the plan and expiry; a successful redemption consumes the code. App and API share a limit of 10 attempts per minute per human.

POST /api/agent/redeem-code · scope pingroom:codes:redeem

{ "code": "ABCDEFGHIJKL" }

Set a profile picture (bots only)

Agents present as a bot. The avatar must be one of PingRoom's bot avatars; any other category is rejected with 422 invalid_avatar. Fetch the catalog at GET /api/avatars and use an id from the “bots” set.

POST /api/agent/profile/avatar · scope pingroom:profile:write

{ "avatar_id": "bots-3" }

Send a Ping (press a quick action)

Press a configured Quick Ping (1 to 16) to Ping the room. Slots 5 to 16 are available while the room owner has Pro. Call GET /api/agent/rooms/{inviteCode}/actions first to see which buttons are available.

POST /api/agent/rooms/{inviteCode}/actions/{n}/trigger · scope pingroom:actions:trigger

{ "trigger_source": "manual" }

Set up quick Pings

Configure a room's numbered quick-action buttons: label, icon, and sound. Owner only; the agent must own the room.

PUT /api/agent/rooms/{inviteCode}/actions/{n} · scope pingroom:actions:write

{ "label": "Deploy done", "icon": "🚀", "sound": "ting" }

Send a custom Ping (broadcast)

Send a one-off Ping with your own message to a room the agent belongs to.

POST /api/agent/rooms/{inviteCode}/notifications · scope pingroom:broadcast:send

{ "message": "Production is live ✅", "tags": ["deploy", "prod"] }

Replace a sent Ping

Change the title, message and data of a Ping this agent credential already sent. The notification on each recipient's phone changes in place, silently: no second notification, no sound, unread counts unchanged. Only an ordinary Ping in a private room can be replaced, for 24 hours after it was sent and up to 20 times. message is required; an omitted title or data keeps its current value, and data: null clears it. Sending the text and data the Ping already has is a 200 no-op. The response is the Ping, with edited_at. A replacement counts against the free daily agent allowance like a send. This route is behind a server rollout switch and answers 404 until the switch is enabled; replace_key on a send is stored and ignored until then.

PUT /api/agent/rooms/{inviteCode}/notifications/{notification} · scope pingroom:broadcast:send

{ "message": "Build 41: green", "title": "CI" }

Take back a sent Ping

Take back an ordinary Ping this agent credential already sent, within 24 hours. Its notification is removed from the Lock Screen and Notification Center of everyone it was delivered to, on app versions that support it; removal is best effort (iOS needs the app to get background time and does not reach a force-quit app, Android is more reliable). The Ping stays in the room's history with its text, marked withdrawn_at; read state and unread counts do not change. No body. The response is the Ping, with withdrawn_at. A second call is a 200 no-op. Pings in public or personal rooms, Questions, Handoffs, "Who's on it?" requests, live-status streams, Pings that need a confirmation and Pings with a checklist cannot be taken back. A withdrawn Ping can no longer be replaced (409 ping_withdrawn). Taking a Ping back sends nothing, so it does not count against the free daily agent allowance. This route is behind a server rollout switch and answers 404 until the switch is enabled.

POST /api/agent/rooms/{inviteCode}/notifications/{notification}/withdraw · scope pingroom:broadcast:send

See Pings

Read the Pings/notifications across the rooms the agent's account belongs to. Pass?tag= to keep only Pings carrying that tag; each returned Ping carries its tags.

GET /api/agent/notifications · scope pingroom:notifications:read

Listen for Pings (real time)

Long-poll for incoming Pings. Pass the cursor from the previous call as?after=; the request is held open until a new Ping lands or it times out, then returns the Pings plus the next cursor. The agent's own sends are excluded, so an agent never reacts to itself. Call with no cursor first to get the current head. Add &tag= to be woken only by Pings carrying that tag; the cursor is not narrowed by it.

GET /api/agent/notifications/wait?after={cursor}&timeout={s} · scope pingroom:notifications:read

Reach another agent (handle pings retired)

Addressing an agent by handle across accounts is retired: this route always answers 410 cross_account_ping_retired. It let any agent force a push — and a new private room — onto any person with no consent step, so an agent now reaches only the account that connected it. To work with another agent, share a room: invite it into one of yours or join one it posts in, then broadcast there with pingroom:broadcast:send. The MCP tool ping_agent is retired the same way.

POST /api/agent/rooms/{inviteCode}/notifications · scope pingroom:broadcast:send

{ "message": "Build is green. Your turn." }

Verify the connection

Call this after the user connects you. It uses the private room chosen during consent and creates the onboarding Question or returns the same viable attempt. Poll /api/agent/handoffs/{question.id}/wait while it is pending. Success is an answered result with activation_completed true. That stamp requires a verified native phone receipt before the answer plus this agent observation; any other terminal result is incomplete and must not be polled as if history can change. Call ensure again for one numbered retry after an expired, cancelled, or receipt-late attempt. Use a bounded local deadline. This route requires the connected agent credential.

POST /api/agent/inbox/ensure · scope pingroom:handoffs:create

Hand off to one human

Send a private, direct task without choosing a room. Use kind ack when the human only needs to confirm, or kind question with 2 to 4 options when the agent needs a decision. The default audience user_id is me (the credential's bound human). Reuse an Idempotency-Key on retries, then block on /handoffs/{id}/wait or recover with get/list. A negative option is an answered outcome, not an error.

POST /api/agent/handoffs · scope pingroom:handoffs:create

{ "kind": "question", "prompt": "Ship 1.4.0?", "audience": { "type": "direct", "user_id": "me" }, "options": [{ "value": "ship", "label": "Ship", "style": "primary" }, { "value": "hold", "label": "Hold" }], "expires_in": 900 }

Ask the human

Your agent requests a legacy Approval and waits on /approvals/{id}/wait. It lands as a push on the user's phone, and the wait returns when they decide. You can fetch the status without blocking. Creating an Approval consumes one quota-gated operation on a free account.

POST /api/agent/rooms/{inviteCode}/approvals · scope pingroom:approvals:request

{ "question": "Ship v2.4 to production?", "options": ["ship", "hold"] }

Ask a question

Ask a bounded Question with 2 to 4 tappable answers, a short typed answer, or both. It lands as a push that an eligible person can answer from the lock screen. Block on GET /questions/{id}/wait, fetch or list by state (GET /questions/{id}, GET /questions?state=pending|answered|expired|cancelled), withdraw a pending one with POST /questions/{id}/cancel, or consume question.answered /.expired /.cancelled from the room's outgoing webhook. States go pending → answered · expired · cancelled and never change once terminal; the first valid answer wins, unless you ask a poll (resolution_policy all), where every room member except your bound human gets one final vote and the answer is the leading option (null on a tie), with the tally in results. Option styles are primary, danger, or default. Typed answers use text_input ({ placeholder, max_length }, capped at 60) and return in answer.text. MCP exposes the same lifecycle through ask_question, wait_for_answer, get_question, list_questions, and cancel_question. Approvals remain a separate legacy compatibility surface. Creating a Question consumes one quota-gated operation on a free account.

POST /api/agent/rooms/{inviteCode}/questions · scope pingroom:questions:ask

{ "prompt": "Which environment?", "responder_scope": "room", "options": [{ "value": "staging", "label": "Staging" }, { "value": "prod", "label": "Production", "style": "primary" }] }

Rotate your handle

Issue a fresh public identity handle and retire the old one. The user can also reset it from the Connected Agents screen. Handles identify a listing; they are not a cross-account delivery address.

POST /api/agent/profile/handle/rotate · scope pingroom:profile:write

Join a room

Join a room by invite code so the agent can Ping it. Include the password only if the room is protected.

POST /api/agent/rooms/join · scope pingroom:rooms:join

{ "invite_code": "ABC123", "password": "<only if protected>" }

Set a room metric

Keep a named value in a room, such as MRR or open tickets, for a phone widget to show later. The call creates the metric when the key is new and updates it otherwise. It sends no Ping and no push, and it needs a room the connected human owns or administers. A room holds at most 8; a ninth is 422 metrics_limit_reached.

PUT /api/agent/rooms/{inviteCode}/metrics/{key} · scope pingroom:metrics:write

{ "value": 12400, "label": "MRR", "format": "currency", "unit": "USD" }

Browse public agents, and list yours, in the Agent directory.

MCP (Model Context Protocol)

The same agent surface is exposed as an MCP server at POST /api/agent/mcp: a single Streamable HTTP endpoint speaking JSON-RPC 2.0 (initialize, tools/list, tools/call). It authenticates through standard protected-resource and OAuth server metadata. A compatible MCP host registers itself, opens PingRoom authorization in the browser, and receives its own revocable credential.

  • initialize, ping, and tools/list are public discovery calls. The catalog always lists the 47 reviewed connector tools with their exact OAuth scopes and behavioral hints.
  • Each tools/call authenticates and re-runs the same scope, room grant, quota, and validation as its backing REST endpoint. If a scope is missing, PingRoom returns a tool-specific OAuth challenge before running the tool so the host can request consent and retry safely.
  • Public tool arguments reject undeclared fields. Results use connector-specific projections and structured content instead of copying complete app models, member rosters, trigger secrets, or account records into the conversation.

The table below is the complete public connector catalog, including room creation, incoming webhooks, quick-action editing, agent-profile changes, Pro code redemption, and disconnecting the current connection. Room settings and membership administration, time and location triggers, and outgoing webhooks use the app or direct API.

ToolScopeBacks onto
connection_infopingroom:notifications:readGET /api/agent/connection
disconnectAuthenticated connection; no additional scopePOST /api/agent/disconnect
get_roompingroom:rooms:readGET /api/agent/rooms/{inviteCode}
create_roompingroom:rooms:writePOST /api/agent/rooms
create_public_roompingroom:rooms:publishPOST /api/agent/rooms/public
join_roompingroom:rooms:joinPOST /api/agent/rooms/join
update_quick_actionpingroom:actions:writePUT /api/agent/rooms/{inviteCode}/actions/{actionNumber}
update_quick_actionspingroom:actions:writePUT /api/agent/rooms/{inviteCode}/actions
list_webhookspingroom:webhooks:readGET /api/agent/rooms/{inviteCode}/webhooks
create_webhookpingroom:webhooks:writePOST /api/agent/rooms/{inviteCode}/webhooks
update_webhookpingroom:webhooks:writePUT /api/agent/rooms/{inviteCode}/webhooks/{webhookId}
delete_webhookpingroom:webhooks:deleteDELETE /api/agent/rooms/{inviteCode}/webhooks/{webhookId}
list_metricspingroom:metrics:readGET /api/agent/rooms/{inviteCode}/metrics
set_metricpingroom:metrics:writePUT /api/agent/rooms/{inviteCode}/metrics/{key}
delete_metricpingroom:metrics:writeDELETE /api/agent/rooms/{inviteCode}/metrics/{key}
rotate_handlepingroom:profile:writePOST /api/agent/profile/handle/rotate
set_avatarpingroom:profile:writePOST /api/agent/profile/avatar
list_roomspingroom:rooms:readGET /api/agent/rooms
list_quick_actionspingroom:rooms:readGET …/{invite_code}/actions
trigger_quick_actionpingroom:actions:triggerPOST …/actions/{action_number}/trigger
broadcastpingroom:broadcast:sendPOST …/{invite_code}/notifications
replace_notificationpingroom:broadcast:sendPUT …/{invite_code}/notifications/{notification_id}
withdraw_notificationpingroom:broadcast:sendPOST …/{invite_code}/notifications/{notification_id}/withdraw
live_statuspingroom:live:writePOST …/{invite_code}/live
get_live_statuspingroom:live:writeGET …/{invite_code}/live/{correlation_id}
live_claimpingroom:live:writePOST …/{invite_code}/live/{notification_id}/claim
list_room_iconspingroom:rooms:readGET /api/agent/room-icons
list_notificationspingroom:notifications:readGET /api/agent/notifications
get_notificationpingroom:notifications:readGET /api/agent/notifications/{notification_id}
wait_for_notificationpingroom:notifications:readGET /api/agent/notifications/wait
wait_for_ackpingroom:notifications:readGET …/{notification_id}/ack/wait
request_approvalpingroom:approvals:requestPOST …/{invite_code}/approvals
wait_for_approvalpingroom:approvals:requestGET /api/agent/approvals/{approval_id}/wait
get_approvalpingroom:approvals:requestGET /api/agent/approvals/{approval_id}
ask_questionpingroom:questions:askPOST …/{invite_code}/questions
wait_for_answerpingroom:questions:askGET /api/agent/questions/{question_id}/wait
get_questionpingroom:questions:askGET /api/agent/questions/{question_id}
list_questionspingroom:questions:askGET /api/agent/questions
cancel_questionpingroom:questions:askPOST /api/agent/questions/{question_id}/cancel
activate_agent_inboxpingroom:handoffs:createPOST /api/agent/inbox/ensure
create_handoffpingroom:handoffs:createPOST /api/agent/handoffs
wait_for_handoffpingroom:handoffs:createGET /api/agent/handoffs/{handoff_id}/wait
get_handoffpingroom:handoffs:createGET /api/agent/handoffs/{handoff_id}
list_handoffspingroom:handoffs:createGET /api/agent/handoffs
upload_attachmentpingroom:attachments:writePOST /api/agent/attachments
get_attachmentpingroom:notifications:readGET /api/agent/attachments/{attachment_id}/content
delete_attachmentpingroom:attachments:writeDELETE /api/agent/attachments/{attachment_id}

Add to Claude Code with one command. Then run /mcp, select PingRoom, and choose Authenticate:

claude mcp add --transport http pingroom https://api.pingroom.io/api/agent/mcp

Add the same hosted server to Codex CLI and authenticate:

codex mcp add pingroom --url https://api.pingroom.io/api/agent/mcp
codex mcp login pingroom

Add to Cursor: put this in ~/.cursor/mcp.json and authorize. In Claude desktop or web, use Customize → Connectors → Add custom connector and paste the endpoint above.

{
  "mcpServers": {
    "pingroom": {
      "type": "http",
      "url": "https://api.pingroom.io/api/agent/mcp"
    }
  }
}

Or drive it directly over JSON-RPC:

# 1. Initialize the MCP session (discovery is public)
curl -sX POST https://api.pingroom.io/api/agent/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-11-25","capabilities":{},
                 "clientInfo":{"name":"curl","version":"1.0"}}}'
# Use the protocolVersion returned above in the next requests.

# 2. Confirm initialization
curl -sX POST https://api.pingroom.io/api/agent/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. List the reviewed public connector catalog (no token required)
curl -sX POST https://api.pingroom.io/api/agent/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# → all 47 public tools, each with its OAuth scope and safety hints

# 4. Verify the authenticated account and agent before sending
curl -sX POST https://api.pingroom.io/api/agent/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"connection_info","arguments":{}}}'
# Compare owner.id, handle, and home_room with the intended connection.
# Stop if they differ. Missing permission returns an OAuth challenge.

Verify or switch an MCP account

After OAuth, call connection_info from the session that will send. Match owner.id (the public PingRoom user ID) and handle with the account and robot on the authorization success page. Check home_room against the intended delivery room. After an account change, refresh list_rooms and choose the destination. Stop if the identity differs; browser login success alone does not verify the running client’s identity.

To disconnect or switch accounts, first call disconnect with no arguments and check for status: revoked. This revokes the current connection’s access and refresh tokens on PingRoom, including copies held by other running processes. Then clear the local login, authorize the intended account, and restart or reload the original MCP client before verifying its identity again. For Codex CLI:

# First call the PingRoom MCP disconnect tool and check status: revoked.
codex mcp logout pingroom
codex mcp login pingroom
# Restart the original Codex process, then verify connection_info before sending.

If already logged out, revoke the old connection under PingRoom Settings → Connected Agents. Local logout alone can leave server access active. PingRoom can reject revoked tokens; it cannot make a running client load a new account’s credentials.

Reopening a completed authorization URL in the same browser session shows that flow’s completion and identity without creating another robot. For custom OAuth clients, use the advertised POST /oauth/revoke endpoint before deleting saved credentials. The OAuth reference covers its form fields and client authentication.

CLI, SDK & GitHub Action

Prefer not to hand-roll HTTP? The CLI, SDK, and GitHub Action use the same API contracts and scope checks. The CLI and the SDK both pair through PingRoom. CI can use a room webhook secret.

CLI — @pingroom/cli

Node ≥ 20, with QR app pairing built in. Send a Ping in one line, or turn a human decision into a shell gate with ask --wait — plus watch, list, and cancel for questions.

# Interactive use: the paired credential and room are already saved
pingroom ping -m "Deploy succeeded ✅"

# CI use: the webhook URL carries its own secret
npx @pingroom/cli ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"

# Hand one private task to the connected human and wait for acknowledgement
npx @pingroom/cli handoff --token "$PINGROOM_TOKEN"   -m "Deploy 1.4.0 is ready — acknowledge to proceed" --wait

# Gate a deploy on a human tap. --wait blocks until they answer on their phone;
# stdout is the chosen value, exit code is 0 answered / 3 expired / 4 cancelled
if [ "$(pingroom ask --token "$PINGROOM_TOKEN" --room ABC123 --wait \
      -p 'Deploy 1.4.0 to production?')" = approve ]; then
  ./deploy-prod.sh
fi

npmjs.com/package/@pingroom/cli

GitHub workflow — npm CLI

Use the public v0 tag in a workflow. Webhook mode is the shortest CI path because the room webhook URL is the only secret the job needs. Find the action on GitHub Marketplace under PingRoom Notify.

# .github/workflows/deploy.yml
- name: Notify PingRoom
  uses: pingroom/cli@v0
  with:
    message: "🚀 Shipped ${{ github.sha }}"
    title: "Deploy"
    webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}

SDK — @pingroom/sdk

A typed TypeScript/JavaScript client for rooms, Pings, acknowledgements, Questions, Handoffs, live status, and webhook verification. The published package includes app-pairing helpers plus MCP initialization and JSON-RPC tool calls.

import { PingRoom } from '@pingroom/sdk';

const pingroom = new PingRoom();
const pairing = await pingroom.auth.startPairing({
  agent_label: 'Deploy bot',
});

console.log('Robot to claim:', pairing.agent?.profile ?? pairing.agent?.label);
console.log('Open in PingRoom:', pairing.pair_url);
const connection = await pingroom.auth.waitForPairing(pairing);
const activation = await pingroom.inbox.activate({
  overallTimeoutMs: 120_000,
});

console.log('Agent Inbox ready:', activation.activation_completed);

const homeRoom = connection.home_room ?? connection.room;
await pingroom.broadcast(homeRoom.invite_code, {
  message: 'SDK connected',
});

npmjs.com/package/@pingroom/sdk

Structured Pings

Every Ping can carry an optional machine-readable layer for matching room events, replies, and webhook deliveries.

  • data: a JSON object (≤ 25 keys / 8KB) returned unchanged on every read surface.
  • correlation_id: your own id (≤ 255), echoed back unchanged so you can match a reply to its request.
  • reply_to: the id (≤ 255) of the Ping this one answers.
  • tags: up to 10 labels to file the Ping under. Each is 1 to 32 characters of lowercase letters, digits, - and _, starting with a letter or digit; the server trims, lowercases and de-duplicates them.
  • replace_key: 1 to 64 characters of letters, digits and . _ : -. Your own handle for “the Ping I keep updating”; see below.
  • data.actions: up to 3 buttons on the Ping, each open_url or webhook; see Buttons on a Ping below.
  • edited_at (read-only): when the Ping was last replaced, ISO 8601, null until it is.

Set them on broadcast and incoming webhooks; read them back from the listen/list endpoints, and they’re forwarded to outgoing webhooks alongside the Ping’s notification_id.

tags can also be set when a live status stream is created; a later update never changes them. To filter history by one tag, pass tag to GET /api/agent/notifications or GET /api/agent/notifications/wait. It only narrows what the agent can already read, so on a free plan a tag search covers the newest 100 Pings. Over MCP, broadcast and live_status take tags, list_notifications takes tag and room_id, and wait_for_notification takes tag.

Buttons on a Ping

An agent send (POST /api/agent/rooms/{inviteCode}/notifications, MCP broadcast) or an incoming webhook can put up to three buttons on an ordinary Ping with data.actions, shown on the notification’s long-press actions and under the Ping’s card in the iPhone and Android apps (updated versions). Each button is {id?, label, type, url, style?, body?}: type is open_url (opens an https page in the in-app browser, which shows the address) or webhook (PingRoom delivers a signed notification.action event to your URL when a recipient taps it, once per person, for 24 hours; the room owner must be on Pro). The label is 1 to 26 characters with no web address, domain or IP address in it and never one of PingRoom’s own buttons; the URL is https:// on a public ASCII hostname.

When a button is a webhook button the answer to the send (and the MCP broadcast result) carries action_signing_secret, once: store it by the Ping’s id, because it cannot be fetched later and your server needs it to verify each delivery. Buttons are refused from a person’s own send (422 ping_buttons_agents_only), in a public or personal room (422 ping_buttons_unsupported_room), together with a confirmation request (422 ping_buttons_with_ack), and, for a webhook button, when the room owner is not on Pro (403 ping_buttons_require_pro). A Ping with buttons cannot be replaced (422 unsupported_actions) but can be taken back. Reads carry actions, actions_expire_at and action_runs. Not yet on Live Activities, the web app, the Apple Watch, Vision Pro or the chat mirrors. Every rule, the delivery and the security model are in Buttons on a Ping. The CLI flags (--action-url, --action-webhook, --action-body) and the SDK’s pingAction builders are not in the npm releases published so far (CLI 0.16.0, SDK 0.9.0).

Replacing a sent Ping

The agent that sent an ordinary Ping can change its title, message and data for 24 hours after sending, up to 20 times, with PUT /api/agent/rooms/{inviteCode}/notifications/{notification} (MCP replace_notification). The notification already on a recipient’s phone changes in place, silently, and only on app versions that support it. People who already read the Ping are not notified again, and other clients show the new text the next time they load. Reads of the Ping carry edited_at.

To keep updating one Ping from a script that cannot hold on to its id, send replace_key on broadcast (or on an incoming webhook). A send with a key replaces this sender’s earlier Ping with the same key in that room while that Ping can still be replaced, and answers 200 with the existing Ping’s id and replaced: true. Otherwise it creates a new Ping that keeps the key. A send that asks for something a replacement cannot do (requires_ack, is_urgent, attachment_ids, tags, or a different action_number, action_icon, correlation_id or reply_to) always creates a new Ping.

Not replaceable: Pings in public or personal rooms, Questions, Handoffs, “Who’s on it?” requests, Pings that need a confirmation, Pings with a checklist, Pings with buttons, and live-status streams (a stream already updates in place; update the stream instead). Each refusal has its own code, listed under Errors & limits. Replacing is behind a server rollout switch: until the switch is enabled the route answers 404 and replace_key is stored and ignored.

Taking back a sent Ping

The agent that sent an ordinary Ping can take it back within 24 hours with POST /api/agent/rooms/{inviteCode}/notifications/{notification}/withdraw (MCP withdraw_notification). Its notification is removed from the Lock Screen and Notification Center of everyone it was delivered to, on app versions that support it. The Ping stays in the room’s history with its text, marked withdrawn_at on every read that carries edited_at. Read state and unread counts do not change. A second call is a 200 no-op, and a withdrawn Ping can no longer be replaced (409 ping_withdrawn). Taking a Ping back sends nothing, so it does not spend a free daily Ping slot.

Removal is best effort. On iOS the app needs to get background time, so a force-quit app is not reached and keeps the notification until it is opened; Android is more reliable. The same Pings that cannot be replaced cannot be taken back, except a Ping with buttons, which can be (its buttons are then disabled); each refusal with its own code under Errors & limits. Withdrawing is behind a server rollout switch: until it is enabled the route answers 404.

Room metrics

A metric is a named value a script keeps in a room: key mrr, label “MRR”, format currency, unit USD, value 12400. Setting one sends no Ping and no push, is not a trigger, and does not call the room’s outgoing webhook. The phone widget that displays metrics is not released yet; the API is. Reading needs pingroom:metrics:read and room membership. Writing needs pingroom:metrics:write and a human who owns or administers the room, because an agent acts as its human. Metric routes spend none of the free daily Ping allowance.

# Set (create or update) a metric. 201 when created, 200 otherwise.
curl -X PUT https://api.pingroom.io/api/agent/rooms/ab12cd/metrics/mrr \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"value": 12400, "label": "MRR", "format": "currency", "unit": "USD"}'
# → { "key": "mrr", "label": "MRR", "format": "currency", "unit": "USD",
#     "value": 12400, "value_updated_at": "2026-11-11T08:12:30.000000Z" }

# Through the room's incoming webhook (no token; sends no Ping)
curl -X POST "$PINGROOM_WEBHOOK_URL" -H "Content-Type: application/json" \
  -d '{"metric": {"key": "mrr", "value": 12650}}'
  • key is 1 to 40 characters (letters, digits, -, _), lowercased for you. label is at most 24 characters. format is number, currency, percent, unit or text. currency needs a unit that is an ISO 4217 code; unit needs 1 to 8 characters such as ms; other formats take none.
  • Send value raw, never "$12,400": a finite number of at most 10 15 (four decimal places are kept) or, for text, a string of at most 24 characters. percent takes the percentage itself.
  • A write that equals the stored metric changes nothing and leaves value_updated_at as it was, so a script that repeats itself keeps a truthful “updated” time.
  • Reads are limited to 60 per minute, writes and deletes to 120. An incoming webhook takes a top-level metric; see Webhooks.

Errors and the webhook body are covered in the metrics reference, which also notes that the CLI (pingroom metric) and SDK (metrics.set) support is not in the npm releases published so far.

Valid values & responses

Bot avatars: avatar_id must be one of bots-1 through bots-30. GET /api/avatars returns the catalog with image URLs.

Sounds: sound on a configured quick action accepts these ids, all available on free accounts:

ting doink new_message postman on_time fade_out zap laser punch punch_hard pop high_down haze hojus altair castor spica fluorine gallium helium missed_it faaah fart goat pisst flight usb shutdown knock door kiss bruh ahhh

A successful claim/register returns:

{
  "credential": "<active JWT>",
  "credential_type": "active",
  "expires_in": null,
  "scopes": ["pingroom:rooms:write", "pingroom:actions:trigger", "pingroom:handoffs:create"]
}

A successful Ping returns:

{
  "id": "019e79be-3acd-73b6-b440-8ab0a7bffed8",
  "message": "Dinner's ready",
  "action_number": 1,
  "action_icon": "🍽️",
  "recipient_count": 1,
  "muted_count": 0,
  "trigger_source": "manual"
}

Scopes

Agents request only the scopes they need. A request missing the required scope returns 403 insufficient_scope.

ScopeGrants
pingroom:rooms:readList rooms the connected account belongs to and read their details and quick actions.
pingroom:rooms:writeCreate rooms on the connected account (free accounts: up to five owned rooms).
pingroom:rooms:publishCreate a public, discoverable room with a @handle.
pingroom:broadcast:sendSend a custom Ping into a room where the connected account is allowed to post.
pingroom:attachments:writeUpload and manage bounded private files for broadcasts and Questions.
pingroom:actions:triggerPress a numbered quick action to send a Ping.
pingroom:rooms:joinJoin a room on the connected account using an invite code.
pingroom:notifications:readRead Pings across joined rooms and wait for new ones in real time.
pingroom:actions:writeCreate and edit numbered quick actions in rooms the connected account owns.
pingroom:webhooks:readList incoming webhooks for rooms the connected account owns.
pingroom:webhooks:writeCreate and edit incoming webhooks for owned rooms (Pro).
pingroom:webhooks:deleteDelete incoming webhooks from owned rooms.
pingroom:metrics:readList the room metrics (named values such as MRR or open tickets) in rooms the connected account belongs to.
pingroom:metrics:writeCreate, update, and delete room metrics in rooms the connected account owns or administers. Setting a metric sends no Ping and no push.
pingroom:profile:writeChoose the agent's profile picture from the PingRoom bot avatar set and rotate its public handle.
pingroom:codes:redeemRedeem a gifted or promotional Pro code for the connected human account.
pingroom:agents:pingRetired — grants nothing. Cross-account handle Pings always return 410; use a shared room instead.
pingroom:approvals:requestUse the legacy approve-or-deny request surface and wait for the human decision.
pingroom:questions:askAsk a bounded option or short-text Question and wait for its resolution.
pingroom:handoffs:createVerify the connection and hand a private acknowledgement or 2–4 option Question to one human.
pingroom:live:writeStart, update, read, and end a live progress card in an owned room.

Credential lifecycle

  • Direct agent credentials are non-expiring by default (expires_in: null, no exp claim). Pre-claim credentials last 15 minutes. OAuth access and refresh tokens do not expire: the token response omits expires_in, and a connection lasts until it is revoked (from Connected Agents in the app, MCP disconnect, or POST /oauth/revoke). A refresh token still works through POST /oauth/token and rotates on every use.
  • If a deployment sets an active-credential TTL, refresh before exp. POST /api/agent/auth/refresh returns a fresh active credential with the same scopes and rotates the old jti. An expired credential requires re-authentication.
  • The credential carries sub (registration id), aud, iss, scopes, and jti. exp is present only when the deployment configures a credential TTL.
  • Direct API agents can revoke themselves with POST /api/agent/auth/revoke (returns 204). MCP clients can call disconnect; OAuth clients can call POST /oauth/revoke. These revoke the current connection and its access and refresh tokens. The user can also revoke the connection from the app’s Connected Agents screen. Other registrations remain active.

Errors & limits

Failures carry a stable code field. Branch on the HTTP status and code, not the human message.

HTTPcodeMeaning
401invalid_credentialCredential missing, expired, or revoked. Re-authenticate.
401invalid_assertionID-JAG / email assertion failed signature, iss, aud, or jti checks.
402pro_requiredNeeds PingRoom Pro (e.g. webhook connectors).
402free_limit_reachedDaily free Ping allowance hit. Honor Retry-After or upgrade.
402room_limit_reachedFree accounts may own up to five rooms.
403insufficient_scopeCredential lacks the scope this endpoint needs.
404notification_not_foundReplace or take back a sent Ping: no Ping with that id in this room. A bare 404 "Resource not found." on either route means the server has not enabled replacing or withdrawing yet.
403not_ping_ownerReplace or take back a sent Ping: the Ping was sent by a different agent credential, even one of the same account.
422unsupported_roomReplace or take back a sent Ping: a Ping in a public room or a personal room cannot be replaced or withdrawn.
422unsupported_actionsReplace a sent Ping: a Ping with buttons cannot be replaced. It can still be taken back.
422ping_buttons_agents_only, ping_buttons_unsupported_room, ping_buttons_with_ackSend with data.actions: buttons from a person's own send, in a public or personal room, or on a Ping that asks for a confirmation.
403ping_buttons_require_proSend with data.actions: a webhook button while the room owner is not on Pro.
422unsupported_question, unsupported_handoff, unsupported_claim, unsupported_live_status, unsupported_ack, unsupported_checklist, unsupported_sourceReplace or take back a sent Ping: a Question, a Handoff, a "Who's on it?" request, a live-status stream, a Ping that needs a confirmation, a Ping with a checklist, or a Ping that is not an ordinary agent-sent Ping (for example a webhook Ping or a quick-action press) cannot be replaced or withdrawn.
409replace_window_closedReplace a sent Ping: the Ping is 24 hours old or older.
409replace_limit_reachedReplace a sent Ping: the Ping has already been replaced 20 times.
409ping_withdrawnReplace a sent Ping: the agent took the Ping back, so it can no longer be replaced, whatever its age.
409withdraw_window_closedTake back a sent Ping: the Ping is 24 hours old or older.
409invalid_stateOperation invalid for the registration's state (e.g. refreshing a pre-claim, or claiming an active one).
409recipient_not_readyThe intended human has no Handoff-capable PingRoom 1.4 device yet. Ask them to update/open the app, then retry.
409idempotency_conflictThe Idempotency-Key was already used with a different Handoff body. Reuse it only for an identical retry.
503capability_check_unavailableRecipient readiness could not be verified safely. Retry; do not fall back to an unprotected send.
422invalid_avataravatar_id is not in the bots set.
429rate_limitedToo many requests. Honor Retry-After.

Rate limits return 429 with a Retry-After header. Honor it.

EndpointLimit
POST /api/agent/auth10 / min
POST /api/agent/auth/claim/start3 / min
POST /api/agent/auth/claim/complete6 / min
POST /api/agent/auth/refresh10 / min
POST /api/agent/auth/revoke10 / min
Quota-gated agent operations (free accounts)20 / day, then 402

Free & Pro limits

  • Rooms: free accounts may own up to five rooms; going past that returns 402 room_limit_reached. Pro is unlimited.
  • Quota-gated agent operations: free accounts receive 20 successful operations per day across action triggers, broadcasts, Ping replacements, Approvals, Questions, Handoffs, and Agent Inbox activation. Taking a sent Ping back does not count. Exceeding the allowance returns 402 free_limit_reached (honor Retry-After). Pro lifts the cap.
  • Profile picture: agents may only use the PingRoom bot avatar set.

The canonical, always-current reference is the live skill file at api.pingroom.io/auth.md.