WhenDocumentation Index
Fetch the complete documentation index at: https://docs.awaithumans.dev/llms.txt
Use this file to discover all available pages before exploring further.
await_human() fires, the default path is a Slack message, an email, or a tab on the awaithumans dashboard. Embedding is a fourth option: render the same form inside your own product, signed into your own auth, on your own domain.
You keep:
- Your auth. The user is already logged into your app; they never see an awaithumans login screen.
- Your shell. The form sits in an
<iframe>you control — header, sidebar, branding, everything outside the form stays yours. - The audit trail. Every submission is recorded against an opaque
subidentifier you choose, alongside the task’s full audit log.
task.completed.
Configure the server
Add two environment variables to your awaithumans server and create a service key.
Mint a token (backend)
On the partner backend, after
await_human() creates the task, mint a short-lived JWT scoped to that task and the iframe’s origin.1. Configure the server
Set two env vars on the awaithumans server:AWAITHUMANS_EMBED_SIGNING_SECRET— HMAC key for the embed JWTs. 32+ bytes of random hex. Without it, the embed feature is off andPOST /api/embed/tokensreturns 404.AWAITHUMANS_EMBED_PARENT_ORIGINS— comma-separated allowlist of iframe parent origins. Drives both theparent_origincheck at mint time and theContent-Security-Policy: frame-ancestorsheader on/embed/*.
2. Mint a token (backend)
After your agent callsawait_human(...) and you have a task.id, mint a token scoped to that task and the iframe’s parent origin.
The token lives in the URL fragment (
#token=...), not the query string. Fragments are never sent in HTTP requests or written to access logs — the iframe reads location.hash client-side and forwards the token in an Authorization header.3. Drop the iframe (frontend)
task.completed handler is slow to update the surrounding page.
Event protocol
The iframe posts messages towindow.parent with targetOrigin pinned to the JWT’s parent_origin claim. Every message includes source: "awaithumans" so you can multiplex multiple iframes through one listener.
type | When it fires | payload |
|---|---|---|
loaded | Task fetched, form rendered | { taskId } |
resize | Iframe content height changed | { height } |
task.completed | User submitted, server accepted | { taskId, response, completedAt } |
task.error | Anything failed (load, submit, expired token) | { taskId, code, message } |
Error codes on task.error
| Code | Meaning |
|---|---|
INVALID_EMBED_TOKEN | Signature failed, expired, or missing from URL |
EMBED_ORIGIN_NOT_ALLOWED | parent_origin not in the server allowlist |
SERVICE_KEY_NOT_FOUND | Service key revoked or unknown (mint endpoint) |
TASK_NOT_FOUND | Task ID in the token doesn’t exist |
TASK_ALREADY_TERMINAL | Task was already completed/cancelled |
internal | Network failure or unhandled exception |
Origin allowlist
AWAITHUMANS_EMBED_PARENT_ORIGINS is comma-separated and matched exactly on scheme, host, and port.
| Allowed | Reason |
|---|---|
Multiple wildcards (https://*.*.acme.com) | ❌ Rejected at server startup |
| Trailing slashes | ❌ Rejected at server startup |
Scheme mismatch (http:// vs https://) | ❌ Mint returns 400 |
Port mismatch (acme.com vs acme.com:8080) | ❌ Mint returns 400 |
frame-ancestors, so browsers refuse to render the iframe inside any other site.
Security model
Service keys live server-side only
Service keys live server-side only
ah_sk_... is the partner secret that authorises minting. Never put it in browser code, mobile app bundles, public env files, or build artifacts. Treat it like a database password.Task payloads are visible to the embed user
Task payloads are visible to the embed user
Whatever you pass as
payload to await_human() is rendered to the human reviewing the task. Don’t put internal-only data, secrets, or PII the partner doesn’t want to expose. Use redact_payload=True on task creation to keep payload server-side only.`sub` is partner-controlled, not verified
`sub` is partner-controlled, not verified
awaithumans records whatever
sub you pass at mint time into the audit row as embed_sub. We don’t verify the identity — that’s the partner’s job (e.g., extract from your own session cookie before minting).`parent_origin` must match the actual iframe parent exactly
`parent_origin` must match the actual iframe parent exactly
https://app.acme.com and https://acme.com are different origins. The server signs the parent_origin into the token, the iframe posts messages with that exact origin, and the browser drops anything mismatched.HTTPS in production, full stop
HTTPS in production, full stop
Mixed content (
http:// iframe inside https:// parent) is blocked by every modern browser. The only http:// origins that work at all are localhost and 127.0.0.1 for local dev.Tokens are short-lived and single-task-scoped
Tokens are short-lived and single-task-scoped
Default TTL is 300 seconds, max 3600. Each token is bound to one
task_id — a leaked token can’t be used to enumerate other tasks. Tampered tokens fail signature verification and return 401 INVALID_EMBED_TOKEN.End-to-end example
A runnable Flask demo lives atexamples/embed/ — a partner backend that creates a task, mints an embed URL, and a parent HTML page that hosts the iframe and listens for task.completed.
Troubleshooting
Iframe loads but shows “Authentication required” —/api/embed/tokens returned 401. The service key is wrong, revoked, or AWAITHUMANS_EMBED_SIGNING_SECRET isn’t set on the server.
Iframe loads but shows a 404 page — the dashboard isn’t bundled into your server image. Self-hosters need to run scripts/build-bundled.sh before pip install. The official wheel ships pre-bundled.
task.error with EMBED_ORIGIN_NOT_ALLOWED — the parent_origin you passed at mint time isn’t in AWAITHUMANS_EMBED_PARENT_ORIGINS. Schemes and ports must match exactly.
postMessage events never fire on the parent — the parent page’s origin doesn’t match the JWT’s parent_origin. Open browser devtools; the iframe page will log the mismatch.