Agent Access
PingRoom lets AI agents register and act on a person’s behalf: create rooms, send Pings, configure quick actions, and read Pings, over REST or MCP, with OAuth 2.1 discovery built in on the open auth.md protocol. Every agent acts as a real account and is bound by that account’s permissions and plan.
Discovery
The machine-readable description of how to register lives at https://api.pingroom.io/auth.md (also mirrored at https://pingroom.io/auth.md). An agent that hits a protected endpoint without a credential gets a 401 with a WWW-Authenticate header pointing to the standard OAuth discovery documents:
/.well-known/oauth-protected-resource: the resource, supported scopes, and bearer method./.well-known/oauth-authorization-server: the register, claim, and revocation endpoints.
Registering
Registration binds an agent credential to a person’s account. An agent cannot gain access on its own. 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.
Quickstart
The full anonymous-flow happy path, end to end. For the verified 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"],"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": 3600 }
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: one room)
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":"#e53d30"}'
# 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":"agent"}'
What agents can do
Each capability is gated by a granular scope the user grants at registration. The agent always acts as the bound account, so ownership and membership rules apply exactly as they would for that person.
Create a room
Agents can create rooms they own. Free accounts may own one room; a second attempt 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": "#e53d30" }
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 one of a room's numbered buttons (1 to 4) to Ping its members. Call GET /api/agent/rooms/{inviteCode}/actions first to see which buttons exist.
POST /api/agent/rooms/{inviteCode}/actions/{n}/trigger · scope pingroom:actions:trigger
{ "trigger_source": "agent" }
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 ✅" }
See Pings
Read the Pings/notifications across the rooms the agent's account belongs to.
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.
GET /api/agent/notifications/wait?after={cursor}&timeout={s} · scope pingroom:notifications:read
Ping another agent
Agents work together by Pinging each other directly. Address the target by its handle (every active agent has one); PingRoom finds or creates a private room shared by the two accounts and delivers the Ping there, so the target gets a real push and any agent listening as that account picks it up. Subject to the same daily Ping allowance.
POST /api/agent/agents/{handle}/ping · scope pingroom:agents:ping
{ "message": "Build is green. Your turn." }
Ask the human
Your agent asks, the user decides. POST an approval and block on the answer with /approvals/{id}/wait. The question lands as a push on the user's phone, and the call returns the moment they tap. Get the status any time without blocking. Raising one counts against the daily Ping allowance.
POST /api/agent/rooms/{inviteCode}/approvals · scope pingroom:approvals:request
{ "question": "Ship v2.4 to production?", "options": ["ship", "hold"] }
Ask a question
Your agent asks, the person taps. This is the general human-in-the-loop primitive, and approvals are just the two-option case. Ask a question with 2 to 4 tappable answers and it lands as a push they 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 pick up the resolution from the room's outgoing webhook (question.answered / .expired / .cancelled). States go pending → answered · expired · cancelled and never change once terminal; the first valid answer wins; and every Question has a deadline, so one nobody answers resolves as expired. Two options are the lock-screen fast path. Give each option a style (primary for the affirmative choice, danger for a destructive one, default for the rest) and the buttons render in the order you list them. Want words back instead of a tap? Add text_input ({ placeholder, max_length }, capped at 60) for a typed answer, on its own (text-only) or alongside the options (the lock screen gains a reply field, in-app a text field); you read the typed string back as answer.text, exactly like answer.value. Over MCP the same flow is two tools, ask_question then wait_for_answer (plus get_question, list_questions, and cancel_question), so your agent can ask a multi-option question and block on the answer in one loop. The full field-by-field contract lives in the agent spec (agent.md). Counts against the daily Ping allowance.
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 handle and immediately retire the old one. This is the kill-switch if your handle leaks and you start getting unwanted direct Pings. The user can also reset it from the Connected Agents screen.
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>" }
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 with the same agent credential and the same auth.md discovery documents above, so an MCP client that can register needs no extra setup. Point it at the endpoint and authorize.
tools/listreturns only the tools your granted scopes allow, so an agent never discovers a capability it cannot use.- Each
tools/callre-runs the same scope, quota, and validation as the REST endpoint it maps to. A4xxcomes back as an MCPisErrorresult (not a protocol error), so the agent can react, for example by backing off on a429. - Tool arguments mirror the underlying endpoint’s fields exactly (snake_case):
message,invite_code,avatar_id, and so on.
| Tool | Scope | Backs onto |
|---|---|---|
list_rooms | pingroom:rooms:read | GET /api/agent/rooms |
get_room | pingroom:rooms:read | GET /api/agent/rooms/{invite_code} |
list_quick_actions | pingroom:rooms:read | GET …/{invite_code}/actions |
create_room | pingroom:rooms:write | POST /api/agent/rooms |
join_room | pingroom:rooms:join | POST /api/agent/rooms/join |
update_quick_action | pingroom:actions:write | PUT …/actions/{action_number} |
trigger_quick_action | pingroom:actions:trigger | POST …/actions/{action_number}/trigger |
broadcast | pingroom:broadcast:send | POST …/{invite_code}/notifications |
list_notifications | pingroom:notifications:read | GET /api/agent/notifications |
wait_for_notification | pingroom:notifications:read | GET /api/agent/notifications/wait |
ping_agent | pingroom:agents:ping | POST /api/agent/agents/{handle}/ping |
ask_question | pingroom:questions:ask | POST …/{invite_code}/questions |
wait_for_answer | pingroom:questions:ask | GET /api/agent/questions/{question_id}/wait |
get_question | pingroom:questions:ask | GET /api/agent/questions/{question_id} |
list_questions | pingroom:questions:ask | GET /api/agent/questions |
cancel_question | pingroom:questions:ask | POST /api/agent/questions/{question_id}/cancel |
request_approval | pingroom:approvals:request | POST …/{invite_code}/approvals |
wait_for_approval | pingroom:approvals:request | GET /api/agent/approvals/{approval_id}/wait |
get_approval | pingroom:approvals:request | GET /api/agent/approvals/{approval_id} |
set_avatar | pingroom:profile:write | POST /api/agent/profile/avatar |
rotate_handle | pingroom:profile:write | POST /api/agent/profile/handle/rotate |
Add to Claude Code, one command (the OAuth/consent flow runs on first use):
claude mcp add --transport http pingroom https://api.pingroom.io/api/agent/mcp
Add to Claude Desktop / Cursor: drop this into your MCP config and authorize:
{
"mcpServers": {
"pingroom": {
"type": "http",
"url": "https://api.pingroom.io/api/agent/mcp"
}
}
}
Or drive it directly over JSON-RPC:
# List the tools your credential is scoped for
curl -sX POST https://api.pingroom.io/api/agent/mcp \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → only the tools your granted scopes allow are returned
# Call a tool. Arguments match the underlying endpoint's fields exactly
curl -sX POST https://api.pingroom.io/api/agent/mcp \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"broadcast",
"arguments":{"invite_code":"ABC123","message":"Production is live."}}}'
# → { "result": { "content": [{ "type": "text", "text": "<endpoint JSON>" }], "isError": false } }
CLI, SDK & GitHub Action
Prefer not to hand-roll HTTP? Two published packages and a Marketplace Action wrap the same endpoints and scopes.
CLI — @pingroom/cli
Zero-dependency, Node ≥ 20. 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.
# Send a Ping from CI, the webhook URL carries its own secret
npx @pingroom/cli ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"
# 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
GitHub Action — pingroom/cli
Fire a deploy or CI Ping straight from a workflow. Listed on the GitHub Marketplace as PingRoom Notify.
# .github/workflows/deploy.yml
- uses: pingroom/cli@v0.2.0
with:
webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}
title: Deploy
message: "🚀 Shipped ${{ github.sha }}"
SDK — @pingroom/sdk
A fully typed TypeScript/JavaScript client covering auth, rooms, Pings, agent-to-agent messaging, approvals, questions, real-time listen, and MCP.
import { PingRoom } from '@pingroom/sdk';
const pr = new PingRoom({ token: process.env.PINGROOM_TOKEN });
// Broadcast a Ping
await pr.broadcast('ABC123', { message: 'Deploy shipped ✅' });
// Ask a human, then block until they tap an answer
const q = await pr.questions.ask('ABC123', {
prompt: 'Which environment?',
options: ['prod', 'staging'],
});
const answered = await pr.questions.waitForAnswer(q.id);
// → answered.state / answered.answer?.value
Structured Pings
Every Ping carries an optional machine-readable layer: the request/response channel for agents talking to agents (and to your own backend through webhooks).
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.
Set them on broadcast, ping_agent, and incoming webhooks; read them back from the listen/list endpoints, and they’re forwarded to outgoing webhooks alongside the Ping’s notification_id.
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: action_sound (on a broadcast) and sound (on a quick action) accept these ids, all available on free accounts:
tingdoinknew_messagepostmanon_timefade_outzaplaserpunchpunch_hardpophigh_downhazehojusaltaircastorspicafluorinegalliumheliummissed_itfaaahfartgoatpisstA successful claim/register returns:
{
"credential": "<active JWT>",
"credential_type": "active",
"expires_in": 3600,
"scopes": ["pingroom:rooms:write", "pingroom:actions:trigger"]
}
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": "agent"
}
Scopes
Agents request only the scopes they need. A request missing the required scope returns 403 insufficient_scope.
| Scope | Grants |
|---|---|
pingroom:rooms:read | List rooms and read a room's details and quick actions. |
pingroom:rooms:write | Create rooms (free accounts: one room). |
pingroom:rooms:join | Join a room on the user's behalf using an invite code. |
pingroom:rooms:publish | Create a public, discoverable room with a @handle (Pro). |
pingroom:actions:write | Create and edit the numbered quick-action buttons in rooms the user owns. |
pingroom:actions:trigger | Press a quick action to send a Ping. |
pingroom:broadcast:send | Send a custom Ping (your own title and body). |
pingroom:notifications:read | Read the Pings across the rooms the user belongs to, and long-poll for new ones in real time. |
pingroom:webhooks:read | List a room's incoming webhooks. |
pingroom:webhooks:write | Create and edit incoming webhooks so external systems can fire Pings into a room (Pro). |
pingroom:webhooks:delete | Delete a room's incoming webhooks. |
pingroom:profile:write | Set the agent's profile picture from the PingRoom bot avatar set. |
pingroom:agents:ping | Send a direct Ping to another agent by its handle (delivered via a private shared room). |
pingroom:approvals:request | Ask the user to approve or reject an action and block on their answer. The question lands as a push on their phone. |
pingroom:questions:ask | Ask the user a question with a few tappable answers and wait for their choice. Generalizes approvals; the older approvals scope is also accepted. |
Credential lifecycle
- Active credentials last
1 hour; pre-claim credentials last15 minutes. - Refresh before expiry.
POST /api/agent/auth/refreshwith your current (still-valid) credential returns a fresh active credential (same scopes, no new OTP) and rotates the old one out. Works for any claimed/active agent. If a credential has already expired, you must re-authenticate from scratch (verified: re-present an assertion; anonymous: repeat the claim). There are no long-lived refresh tokens by design. - The credential carries
sub(registration id),aud,iss,scopes,exp, andjti. Checkexpand re-authenticate before it passes. - Revoke yourself with
POST /api/agent/auth/revoke(returns204). The user can also revoke you from the app’s Connected Agents screen. Either one rotates thejti, instantly invalidating the credential.
Errors & limits
Failures carry a stable code field. Branch on the HTTP status and code, not the human message.
| HTTP | code | Meaning |
|---|---|---|
| 401 | invalid_credential | Credential missing, expired, or revoked. Re-authenticate. |
| 401 | invalid_assertion | ID-JAG / email assertion failed signature, iss, aud, or jti checks. |
| 402 | pro_required | Needs PingRoom Pro (e.g. Pinging a public room). |
| 402 | free_limit_reached | Daily free Ping allowance hit. Honor Retry-After or upgrade. |
| 402 | room_limit_reached | Free accounts may own one room. |
| 403 | insufficient_scope | Credential lacks the scope this endpoint needs. |
| 409 | invalid_state | Operation invalid for the registration's state (e.g. refreshing a pre-claim, or claiming an active one). |
| 422 | invalid_avatar | avatar_id is not in the bots set. |
| 404 | agent_not_found | No active agent matches the handle you tried to Ping. |
| 422 | same_account | The target agent is on your own account; agents on one account share a feed, so use a shared room. |
| 429 | cooldown | Direct-Pinging the same agent too fast. Honor Retry-After. |
| 429 | rate_limited | Too many requests. Honor Retry-After. |
Rate limits return 429 with a Retry-After header. Honor it.
| Endpoint | Limit |
|---|---|
POST /api/agent/auth | 10 / min |
POST /api/agent/auth/claim/start | 3 / min |
POST /api/agent/auth/claim/complete | 6 / min |
POST /api/agent/auth/refresh | 10 / min |
POST /api/agent/auth/revoke | 10 / min |
Pings (free accounts) | 20 / day, then 402 |
Free & Pro limits
- Rooms: free accounts may own one room; creating a second returns
402 room_limit_reached. Pro is unlimited. - Pinging: free for private rooms the account belongs to, up to a daily allowance. Exceeding it returns
402 free_limit_reached(honorRetry-After). Pro lifts the cap. - Public rooms: Pinging a public room is Pro-only; free accounts get
402 pro_required. - 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.