REST reference
Base URL: https://api.pendnt.dev. Every /v1/* route
requires Authorization: Bearer <api_key> and is scoped to that key's
workspace. /in/:slug and /a/:request_id/:sig are public/unauthenticated
by design — see Security for how those are protected instead.
On this page
Conventions
- All request/response bodies are JSON (
content-type: application/json), except the operator answer page, which also accepts HTML form posts. - Timestamps are ISO 8601 strings, e.g.
"2026-08-29T03:15:02Z". wait_s(long-poll) is clamped to 0–25 seconds everywhere it's accepted — Workers requests aren't meant to hang open indefinitely. Poll again, or use a durable id (request_id,since_seq) to keep picking up where you left off across calls.- API keys look like
aio_<32 base62 chars>. See Security for how they're scoped and stored.
Requests (approvals / questions / notifications)
POST/v1/requests
Create an approval or question.
{
"kind": "approval",
"title": "Deploy to prod?",
"details": "release v1.2.3",
"options": ["yes", "no"],
"timeout_s": 86400,
"wait_s": 0
}
kind:approval|question|notification.timeout_s: defaults to 86400 (24h); the request auto-expires (status: "expired") after this.wait_s(0–25): if greater than 0, blocks (long-polling) until answered orwait_selapses, then returns the current state instead of always returningpending.
Response: { "id", "status", "answer" }
GET/v1/requests/:id?wait_s=N
Fetch (and optionally long-poll) a request's current state. Returns the full row:
{
"id", "kind", "title", "details", "options", "status", "answer",
"answered_by", "created_at", "expires_at", "answered_at"
}
POST/v1/requests/:id/cancel
Cancels a pending request (status -> cancelled) and wakes any long-pollers.
No-op (returns the current row) if already resolved.
POST/v1/notify
Fire-and-forget notification.
{ "message": "build finished", "level": "info" }
Creates a kind: "notification" request that's immediately
status: "answered", and enqueues a delivery job. Response:
{ "id", "status": "answered" }
Inbound endpoints (webhooks)
POST/v1/endpoints
{ "name": "gh-webhook", "require_token": false }
Response:
{ "id", "name", "slug", "url", "secret", "require_token", "created_at" }
url is <BASE_URL>/in/<slug> — point any third-party
webhook (GitHub, Stripe, an OAuth callback, ...) at it.
GET/v1/endpoints
Response: { "endpoints": [...] }
DELETE/v1/endpoints/:id
Response: { "ok": true }
ANY/in/:slug
No API key. Stores the request as an event: method, path, query params,
headers (hop-by-hop and Cloudflare-internal headers stripped), and body (up to 256KB; stored as
UTF-8 text when it decodes cleanly and the content-type looks textual, base64 otherwise).
Responds 200 { "ok": true, "event_id": "..." } for any method/body.
If the endpoint was created with require_token: true, the request must include
?token=<secret> or an X-Endpoint-Token: <secret> header, or
it's rejected with 403.
Events
GET/v1/events?endpoint_id=&since_seq=0&wait_s=0
Lists events for the workspace (optionally filtered to one endpoint), ordered by
seq ascending, seq > since_seq. With wait_s > 0,
long-polls until a new matching event arrives or the deadline elapses.
Response: { "events": [...] }, each event:
{ "id", "endpoint_id", "kind", "payload", "headers", "received_at", "seq" }
kind is http (inbound webhook), wakeup (fired scheduled
wakeup), or oauth (reserved — an OAuth callback is just an http event
today).
GET/v1/events/:id
A single event by id.
Wakeups
POST/v1/wakeups
{ "at": "2026-09-01T00:00:00Z", "in_s": 3600, "payload": {} }
Provide exactly one of at/in_s. Schedules an alarm; when it fires, a
kind: "wakeup" event is written (visible via /v1/events, payload
{ wakeup_id, payload }) and, if the workspace has a wake webhook URL configured (see
below), a best-effort POST is sent to it.
GET/v1/wakeups
Response: { "wakeups": [{ id, fire_at, payload, status, created_at }] },
status: scheduled | fired | cancelled.
DELETE/v1/wakeups/:id
Cancels a scheduled wakeup.
PUT/v1/settings/wake-webhook
{ "url": "https://..." } (or { "url": null } to clear). Best-effort
outbound POST fired whenever a scheduled wakeup fires — useful to nudge a
long-running supervisor process that's waiting on more than just wait_for_event.
KV (per-workspace scratchpad)
GET/v1/kv/:key
Response: { "key", "value", "updated_at" } or 404.
PUT/v1/kv/:key
{ "value": "a string, <= 64KB" }
Response: same shape as GET.
DELETE/v1/kv/:key
Response: { "ok": true }
Operator answer page (signed link, no login)
GET/a/:request_id/:sig
Renders a minimal HTML page (title, details, Approve/Deny buttons for approval
requests, one button per options entry for question requests, and
always a free-text box) for a pending request. Shows a "resolved" state instead if the request is
no longer pending, or an expired/invalid message.
POST/a/:request_id/:sig
Records the answer (action: approve | deny |
answer, plus an optional text), sets the terminal status
(approved/denied/answered), and wakes any long-pollers on
that request. Accepts both application/json and
application/x-www-form-urlencoded (the rendered page uses plain
<form> POSTs, no JS required).
sig is base64url(HMAC-SHA256(request_id, SIGNING_SECRET)), verified
in constant time — see Security.
Errors
Errors are JSON: { "error": "<code>", "message": "..." } with a matching
HTTP status — 400 for validation errors, 401 for a missing/invalid API
key, 403 for a missing/invalid endpoint token, 404 for an unknown id
(request, endpoint, event, wakeup, or KV key), 429 for rate limiting.
Rate limits
600 requests/minute per API key (fixed-window counter — allows brief bursts up to roughly 2x across a window boundary). See Pricing for plan-level limits (requests/month, retention, endpoints).