API Reference

Everything you need to add human-in-the-loop approvals to your AI agents.

https://holdon-xlqz.polsia.app/api

Quickstart

1 Get an API key
curl -X POST https://holdon-xlqz.polsia.app/api/keys \ -H "Content-Type: application/json" \ -d '{"name": "my-agent", "telegram_bot_token": "YOUR_BOT_TOKEN", "telegram_chat_id": "YOUR_CHAT_ID"}'
2 Create an approval
curl -X POST https://holdon-xlqz.polsia.app/api/approvals \ -H "Authorization: Bearer ho_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{"action": "Send $4,200 wire to vendor", "context": "Invoice #8821", "notify": ["telegram"]}'
3 Human decides via Telegram or web link
4 Poll or receive webhook
curl https://holdon-xlqz.polsia.app/api/approvals/APPROVAL_ID \ -H "Authorization: Bearer ho_sk_your_key_here"

Authentication

All API requests (except decisions) require an API key in the Authorization header:

Authorization: Bearer ho_sk_your_key_here

API keys start with ho_sk_. The full key is shown once at creation. Store it securely.

Endpoints

POST /api/keys

Create a new API key. No authentication required.

ParameterTypeDescription
namestring requiredKey name (e.g., "my-agent")
telegram_bot_tokenstringTelegram bot token for notifications
telegram_chat_idstringTelegram chat ID to send to
notification_emailstringEmail for approval notifications
webhook_urlstringDefault webhook URL for callbacks

POST /api/approvals

Create an approval request. Sends notifications to configured channels.

ParameterTypeDescription
actionstring requiredWhat needs approval (e.g., "Send $4,200 wire")
contextstringAdditional context (e.g., "Invoice #8821, verified by AP agent")
urgencystringlow | normal | high | critical
notifystring[]Channels: ["telegram"], ["email"], or both
webhook_urlstringOverride webhook URL for this approval
expires_innumberSeconds until expiry (e.g., 3600 = 1 hour)
metadataobjectArbitrary JSON attached to the approval

Response:

{ "id": "a1b2c3d4-...", "status": "pending", "decision_url": "https://holdon-xlqz.polsia.app/decide/TOKEN", "notifications": { "telegram": { "sent": true } }, "expires_at": "2026-04-04T08:20:00Z", "created_at": "2026-04-04T07:20:00Z" }

GET /api/approvals/:id

Check the status of an approval. Poll this endpoint to wait for a decision.

Statuses: pending approved rejected expired

GET /api/approvals

List all approvals for your API key. Supports filtering and pagination.

Query ParamTypeDescription
statusstringFilter by status
limitnumberResults per page (max 100, default 20)
offsetnumberPagination offset

Decision Flow

When an approval is created, a unique decision URL is generated. Humans can approve or reject through:

1. Web page — Click the decision URL to see details and one-click approve/reject.

2. Telegram — Inline buttons open the decision page with the action pre-selected.

3. Email — Green/red buttons link to the decision page.

POST /api/decide/:token

Submit a decision. No authentication required (the token IS the auth).

ParameterTypeDescription
decisionstring requiredapproved or rejected
decided_bystringWho made the decision
notestringOptional note

Webhooks

When a decision is made, HoldOn fires a POST to your webhook_url:

{ "event": "approval.decided", "approval": { "id": "a1b2c3d4-...", "action": "Send $4,200 wire to vendor", "status": "approved", "decided_by": "john@example.com", "decided_at": "2026-04-04T07:25:00Z", "metadata": {} }, "timestamp": "2026-04-04T07:25:00Z" }

Webhooks retry 3 times with exponential backoff (1s, 5s, 15s). Requests timeout after 10 seconds.

Headers include X-HoldOn-Event: approval.decided and X-HoldOn-Approval-ID.

SDK Example

Integrate HoldOn into any Node.js agent:

// holdon.js - minimal client const HOLDON_URL = 'https://holdon-xlqz.polsia.app/api'; const API_KEY = process.env.HOLDON_API_KEY; async function ask({ action, context, notify, webhook_url, expires_in }) { const res = await fetch(HOLDON_URL + '/approvals', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ action, context, notify, webhook_url, expires_in }) }); return res.json(); } async function check(id) { const res = await fetch(HOLDON_URL + `/approvals/${id}`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); return res.json(); } async function waitFor(id, interval = 3000) { while (true) { const approval = await check(id); if (approval.status !== 'pending') return approval; await new Promise(r => setTimeout(r, interval)); } } module.exports = { ask, check, waitFor };
// In your agent const holdon = require('./holdon'); const { id } = await holdon.ask({ action: "Send $4,200 wire to vendor", context: "Invoice #8821, verified by AP agent", notify: ["telegram"] }); const decision = await holdon.waitFor(id); if (decision.status === 'approved') { executeWire(); }