Skip to content

HTTP API overview

Everything the CLI does rides this API, so any language with HTTP can integrate directly.

  • Base URL: https://api.pidge.sh
  • Auth: Authorization: Bearer $PIDGE_TOKEN — the channel key (hld_…) from the app.
  • The canonical spec: GET /api/v1/manifest — self-documenting and versioned (fields, profiles, action catalog, guarantees, whats_new). These pages are the narrative; the manifest is the contract.
{
"title": "Order ready",
"body": "Table for review",
"body_markdown": "| item | qty |\n|---|---|\n| widgets | 12 |",
"template": "important",
"actions": ["approve", "reject"],
"custom_actions": [
{ "id": "send_order", "label": "Send order", "style": "destructive",
"biometric": true, "terminal": true }
],
"correlation_id": "order-7",
"reply_to": "https://your-agent.example/hook",
"deliver_at": "2026-07-14T18:00:00-03:00",
"collapse_key": "order-7",
"action_data": { "task_id": 123 }
}
  • 201 echoes the accepted notification — trust the echo. If the human’s channel policy lowered the loudness you get degraded: true + degrade_reason (still 201); 422 is reserved for contract errors. Nothing is silently dropped.
  • correlation_id is the idempotency key: re-POSTing the same id updates rather than duplicates, and it’s the handle you poll and the key echoed on every reply.
  • A send returns 201 even if no device is registered — treat “never answered within your timeout” as no answer, not failure.
  • A 201 whose send asks for a decision (requires_action) carries nobody_listening: true (present-only) when no live consumer holds the channel’s queue — your answer will queue safely, but nothing will wake you. Start your listen loop, or block on GET /notifications/:cid?wait=. Advisory: delivery is unchanged, the flag never blocks a send.
  • deliver_at in the future schedules a durable server-side job; snooze re-fires are also server-owned.

Poll an answer — GET /api/v1/notifications/:correlation_id

Section titled “Poll an answer — GET /api/v1/notifications/:correlation_id”
Terminal window
curl https://api.pidge.sh/api/v1/notifications/order-7 -H "Authorization: Bearer $PIDGE_TOKEN"
{ "responded": true,
"chosen_action": { "kind": "acted", "action_id": "approve", "label": "Approve",
"text": null, "at": "2026-07-11T17:28:47Z" } }

Add ?wait=25 to long-poll: the server holds the request (capped ~25 s) and answers the moment the human acts. Loop that and you have sub-30-second latency with plain HTTP.

Two behaviors to handle on every answer poll:

  • action_id can be "done" even if you never offered it (v93): the server appends a neutral, terminal Done to every send that offers actions. It means the human cleared the pendency without deciding — treat it like any terminal outcome (stop polling; it is not an approval).
  • Add &wake_on_message=true (channel-key only, v91 — strongly recommended on blocking waits): the same held request also returns early when the channel’s /messages queue holds a deliverable composer message — the human types in the same chat where they tap your buttons. Any return then carries messages_pending: true while the queue is non-empty; on seeing it, drain GET /api/v1/messages?wait=0 and ack after handling. The wake never serves, leases or acks anything — skip the param when a separate bridge owns the queue.

The inbound queue — GET /api/v1/messages

Section titled “The inbound queue — GET /api/v1/messages”

Human-initiated messages (and, with all=true, answers to fire-and-forget notifications) queue per channel until the agent consumes them:

Terminal window
curl "https://api.pidge.sh/api/v1/messages?wait=25&all=true" -H "Authorization: Bearer $PIDGE_TOKEN"
# → { "messages": [ { "id": 812, "kind": "message", "text": "status?", … } ] }
curl -X POST https://api.pidge.sh/api/v1/messages/ack \
-H "Authorization: Bearer $PIDGE_TOKEN" -H "Content-Type: application/json" \
-d '{"up_to": 812, "summary": "answered with the deploy status"}'

Delivery is at-least-once: reading a message starts a ~10-minute visibility lease; ack (up_to) only after durably handling it, or the message is re-served. One consumer per channel — see Waiting & receiving for the consume-vs-peek rules.

If a send carries reply_to, every user action POSTs there:

POST <reply_to>
Content-Type: application/json
X-Pidge-Signature: sha256=<hex HMAC-SHA256 of the raw body>
X-Herald-Signature: sha256=<same value — legacy alias>
{
"type": "herald.event",
"event": "acted",
"action_id": "approve",
"label": "Approve",
"text": null,
"correlation_id": "order-7",
"notification_id": 42,
"title": "Order ready",
"payload": { "action_id": "approve", "label": "Approve" },
"occurred_at": "2026-07-11T17:28:47Z"
}
  • event is one of acted · replied · completed · snoozed · dismissed · muted; snoozed is the only intermediate one (it re-fires) — every other event is the final, one-and-done answer. text carries a free-text reply. On an E2E channel, enc/kf ride along and title is an opaque envelope — see End-to-end encryption.
  • The callback URL must be public HTTPS without embedded credentials — loopback, private and link-local destinations are refused. A local-only agent should poll or use the messages queue instead; they carry the same information.
  • Delivery retries with backoff (about 8 attempts). Treat callbacks as idempotent — key on notification_id + event + occurred_at. Reply text is untrusted user input; validate it before acting on it.
  • The signature is HMAC-SHA256 of the raw body under Pidge’s server-side callback secret. It arrives in two headers with the same value: X-Pidge-Signature (canonical — verify this one) and X-Herald-Signature (legacy alias, kept verbatim for production continuity). Accept either; new integrations should read the canonical name.

Naming note: the wire keeps some pre-rename strings for production continuity — the X-Herald-Signature alias, the "type": "herald.event" field and the hld_ key prefix are intentional, not typos.

Once plan enforcement is live, POST /api/v1/notify can answer 402 with {error, code, message} — and the message is always written to be relayed to your human verbatim. Everything read-side keeps working on your key regardless (answers, the messages queue, whoami, cancel); sending is the only thing that closes.

Code Meaning
subscription_required trial + grace over, no subscription — subscribing in the app resumes delivery instantly; tell your human, then just retry
group_channel_requires_pro group channels are Pro-only and this channel’s owner is on base — nothing is deleted; delivery resumes on upgrade
channel_over_limit base caps active channels at 10 and this one is outside the active set — the human picks the survivors in the app
channel_limit_reached POST /channels at the base cap

GET /api/v1/whoami warns you before any of these bounce: the user block carries plan_state (trial / trial_grace / expired / subscribed / comp) and plan_tier (pro / base) — check them before wiring a new channel or inviting members.

Progress cards have their own three-call API — see Live Activities.

Public without auth (the catalog); with a channel key it adds that channel’s configuration and a versioned whats_new changelog. Every response also carries the manifest version in the X-Pidge-Manifest-Version header. If you build a client, read the manifest at startup — new fields land there first.