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.
Send — POST /api/v1/notify
Section titled “Send — POST /api/v1/notify”{ "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 }}201echoes the accepted notification — trust the echo. If the human’s channel policy lowered the loudness you getdegraded: true+degrade_reason(still201);422is reserved for contract errors. Nothing is silently dropped.correlation_idis 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
201even if no device is registered — treat “never answered within your timeout” as no answer, not failure. - A
201whose send asks for a decision (requires_action) carriesnobody_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 onGET /notifications/:cid?wait=. Advisory: delivery is unchanged, the flag never blocks a send. deliver_atin 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”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_idcan 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/messagesqueue holds a deliverable composer message — the human types in the same chat where they tap your buttons. Any return then carriesmessages_pending: truewhile the queue is non-empty; on seeing it, drainGET /api/v1/messages?wait=0and 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:
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.
Webhook — Pidge POSTs to reply_to
Section titled “Webhook — Pidge POSTs to reply_to”If a send carries reply_to, every user action POSTs there:
POST <reply_to>Content-Type: application/jsonX-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"}eventis one ofacted · replied · completed · snoozed · dismissed · muted;snoozedis the only intermediate one (it re-fires) — every other event is the final, one-and-done answer.textcarries a free-text reply. On an E2E channel,enc/kfride along andtitleis 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) andX-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-Signaturealias, the"type": "herald.event"field and thehld_key prefix are intentional, not typos.
Billing — the 402 family
Section titled “Billing — the 402 family”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.
Live Activities
Section titled “Live Activities”Progress cards have their own three-call API — see Live Activities.
Manifest — GET /api/v1/manifest
Section titled “Manifest — GET /api/v1/manifest”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.
