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.
| Parameter | Type | Description |
| name | string required | Key name (e.g., "my-agent") |
| telegram_bot_token | string | Telegram bot token for notifications |
| telegram_chat_id | string | Telegram chat ID to send to |
| notification_email | string | Email for approval notifications |
| webhook_url | string | Default webhook URL for callbacks |
POST /api/approvals
Create an approval request. Sends notifications to configured channels.
| Parameter | Type | Description |
| action | string required | What needs approval (e.g., "Send $4,200 wire") |
| context | string | Additional context (e.g., "Invoice #8821, verified by AP agent") |
| urgency | string | low | normal | high | critical |
| notify | string[] | Channels: ["telegram"], ["email"], or both |
| webhook_url | string | Override webhook URL for this approval |
| expires_in | number | Seconds until expiry (e.g., 3600 = 1 hour) |
| metadata | object | Arbitrary 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 Param | Type | Description |
| status | string | Filter by status |
| limit | number | Results per page (max 100, default 20) |
| offset | number | Pagination 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).
| Parameter | Type | Description |
| decision | string required | approved or rejected |
| decided_by | string | Who made the decision |
| note | string | Optional 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();
}