Requesting from your app
Ask for an approval from TypeScript, Python or plain HTTP: every field, the answer you get back, the statuses, cancelling, and safe repeats.
This page shows how your app asks for an approval, what comes back, and how to check or cancel it. For how to learn the answer, see callbacks and waiting.
Ask in TypeScript
Generate the typed platform object once, and again whenever you change cohostly.json (the platform object).
cohostly types --project orders-admin
import platform from './platform.gen.js' const approval = await platform.approvals.request({ summary: 'Delete all orders older than 90 days', details: { table: 'orders', older_than_days: 90 }, risk: 'high', action: 'purge_orders', expiresIn: 3600, }) // approval.id is what you keep. approval.status is "pending".
from platform_gen import platform approval = platform.approvals.request({ "summary": "Delete all orders older than 90 days", "details": {"table": "orders", "older_than_days": 90}, "risk": "high", "action": "purge_orders", "expires_in": 3600, # snake_case in Python and over HTTP }) # approval["id"] is what you keep. approval["status"] is "pending".
In Python, run cohostly types --project orders-admin --lang python. It writes platform_gen.py, which holds the platform object. The Python client has request, get, cancel and signingKey. It has no wait and no callback helper, so you check by id or verify the callback yourself.
The fields you send
summarytextdetailsobjectnull is refused. It may be at most 16 KiB (16384 bytes) once written as JSON. Default {}.risktextlow, medium or high. Default medium.actiontextpurge_orders. Lower-case letters, digits and . _ : -, starting with a letter or digit, 1 to 80 characters.expiresInwhole numberexpires_in. Default is the project rule's value, or 86400 (24 hours).callback_pathtextcohostly.json, or be left out. The TypeScript call does not take it.details.quantity is a number that only "Always allow" rules read. Without one, it counts as 1. See who sees and answers.
The fingerprint
The reply carries a details_hash. It is a fingerprint of action, summary, risk and details together. An approver sends it back with their answer, so they are answering exactly what you wrote. Change any one of those and you have made a different approval: one changed byte, a new approval.
What comes back
You get a handle. Over HTTP it is this JSON:
{ "id": "2b0c0b2e-…", "status": "pending", "summary": "Delete all orders older than 90 days", "action": "purge_orders", "risk": "high", "details_hash": "…", "created_at": "2026-10-28T09:00:00Z", "expires_at": "2026-10-28T10:00:00Z", "approvals": 0, "required": 1 }
idtextstatustextsummary, action, risktextaction is left out if you sent none.details_hashtextcreated_at, expires_attextdecided_at, decision_notetextapprovalswhole numberrequiredwhole numbercallbackobject{ path, status }. status is none, pending, delivered or failed.Your app never sees who decided. The reply holds no approver's identity, and neither does a callback.
A pending request that has passed expires_at is reported as expired at once, even before the platform records it.
In TypeScript the handle also has wait(), refresh(), cancel() and onDecision().
Statuses
pendingapproveddeniedexpiredexpires_at. It is never approved afterwards.cancelledcancelled can be read with get, but it is never sent in a callback.
Check or cancel by id
const now = await platform.approvals.get({ id: approval.id }) console.log(now.status, now.expires_at) await platform.approvals.cancel({ id: approval.id })
now = platform.approvals.get(approval["id"]) print(now["status"], now["expires_at"]) platform.approvals.cancel(approval["id"])
Cancelling a request that is already decided fails with not_pending.
Plain HTTP
Any language can call the binding. Send your app's binding token in the X-Platform-Binding-Token header. On the platform it is the PLATFORM_TOKEN setting. Never log it or send it to a browser.
curl -sS -X POST "$APPROVALS_URL" \ -H "X-Platform-Binding-Token: $PLATFORM_TOKEN" \ -H "Idempotency-Key: purge-orders-2026-10-28" \ -H "content-type: application/json" \ -d '{"summary":"Delete all orders older than 90 days","details":{"table":"orders"},"risk":"high","action":"purge_orders","expires_in":3600}' curl -sS "$APPROVALS_URL/<id>" -H "X-Platform-Binding-Token: $PLATFORM_TOKEN" curl -sS -X POST "$APPROVALS_URL/<id>/cancel" -H "X-Platform-Binding-Token: $PLATFORM_TOKEN"
POST $APPROVALS_URL{summary, details?, risk?, action?, expires_in?, callback_path?}. Optional Idempotency-Key header. Answers 200 with the approval.GET $APPROVALS_URL/<id>POST $APPROVALS_URL/<id>/cancelnot_pending if it was already decided.GET $APPROVALS_URL/signing-keyA failed call answers with an error body holding a code, a message and a how_to_fix. Every code is on the errors page. In the SDK, the call throws a PlatformError.
Asking twice: idempotency
Idempotency means "asking twice has the same effect as asking once". It protects you when your app retries after a crash or a slow network.
- Without a key, every
requestmakes a new approval. The SDK adds a random key to each call, but only so that its own network retries are safe. - With a key, the platform remembers it for your project. The same key with the same summary and details returns the original approval. Over HTTP the reply carries the header
Idempotent-Replayed: true. - Same key, different request: a different summary or details fingerprint is refused with
idempotency_conflict. Use a new key for a new request.
In TypeScript, pass the key as a second argument. Build it from the thing you are asking about, such as one ask per day:
const approval = await platform.approvals.request( { summary: 'Delete all orders older than 90 days', details: { table: 'orders' }, risk: 'high', action: 'purge_orders' }, { idempotencyKey: `purge_orders:${new Date().toISOString().slice(0, 10)}` }, )
Over HTTP, send the Idempotency-Key header. Keys are trimmed and only the first 200 characters count. Keys are unique per project, and no expiry for them is documented here, so treat a key as used for good.
Careful The Python client cannot set a key. It makes a fresh random key on every call. To avoid asking twice in Python, store the approval
idin your own database before you move on, or call the HTTP address yourself.
Gotchas
- Keep the id and what it was for. Save the
idtogether with the action and its inputs in your own database. When the answer arrives, act from what you stored, not from the callback body. - Do not hold a web request open for hours. An idle app pauses. Answer the visitor with a
202, store theid, and finish in the callback. See callbacks and waiting. - Ask once per real action. Each project can make 10 requests a minute. See limits.
- Your app does the action. Approved does not mean done.
- Held connection writes are a different flow. A write held by a connection answers
202and uses its own steps. See write approval.