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

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
}

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).

Early access — launched August 2026.