Skip to main content
When 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 sub identifier you choose, alongside the task’s full audit log.
Three pieces wire it together: your backend mints a short-lived token, your frontend drops the URL into an iframe, your frontend listens for task.completed.
1

Configure the server

Add two environment variables to your awaithumans server and create a service key.
2

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

Drop the iframe (frontend)

Render an <iframe> with src = embed_url, listen for task.completed, react.

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 and POST /api/embed/tokens returns 404.
  • AWAITHUMANS_EMBED_PARENT_ORIGINS — comma-separated allowlist of iframe parent origins. Drives both the parent_origin check at mint time and the Content-Security-Policy: frame-ancestors header on /embed/*.
Then create a service key — the partner-side secret used to mint embed tokens:
Service keys are shown once at creation. Store them like database passwords — never commit them, never put them in frontend code. To rotate, create a new key, deploy, then awaithumans revoke-service-key <id>.

2. Mint a token (backend)

After your agent calls await_human(...) and you have a task.id, mint a token scoped to that task and the iframe’s parent origin.
The response contains everything your frontend needs:
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)

After a successful submit the iframe also shows a built-in “Submitted” panel, so users get inline confirmation even if your task.completed handler is slow to update the surrounding page.

Event protocol

The iframe posts messages to window.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.

Error codes on task.error

Origin allowlist

AWAITHUMANS_EMBED_PARENT_ORIGINS is comma-separated and matched exactly on scheme, host, and port.
The same list drives the iframe’s CSP frame-ancestors, so browsers refuse to render the iframe inside any other site.

Security model

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.
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.
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).
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.
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.
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 at examples/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.