callback_url=..., the awaithumans server stops asking you to long-poll. Instead, on every terminal-state transition it POSTs an HMAC-signed JSON body to your URL. This is the foundation the Temporal and LangGraph adapters ride on — but it’s also useful on its own for any service that prefers push over pull.
When to use callback_url vs polling
| You’re building | Use |
|---|---|
| A short-lived script (Flask handler, cron job, REPL session) | SDK long-poll (default — no callback_url needed) |
| A Temporal / LangGraph workflow that may park for hours | The adapter’s callback_url (auto-wired) |
| A serverless function that times out at 15 minutes | callback_url to a separate handler |
| A FaaS or queue-driven worker that shouldn’t hold a connection | callback_url |
Wire format
status is one of completed, timed_out, cancelled, verification_exhausted. The body is self-contained — your receiver doesn’t need a second round-trip.
Signature verification
The signature isHMAC-SHA256(body) using a key HKDF-derived from AWAITHUMANS_PAYLOAD_KEY. Both sides need the same PAYLOAD_KEY value.
Python receiver
verify_signature is keyword-only and reads AWAITHUMANS_PAYLOAD_KEY from the environment — the receiver process needs the same PAYLOAD_KEY value that the awaithumans server signs with. The helper lives in awaithumans.utils.* deliberately: it has no dependency on the [server] extra, so a Temporal worker or LangGraph driver can verify callbacks without installing FastAPI / SQLModel.
TypeScript — using an adapter
The Temporal and LangGraph adapters bundle signature verification into their dispatch helpers — call them directly from your route and you don’t have to write the HMAC code: Both snippets below assume an Express app and the same raw-body helper:Other languages
The signing key is notPAYLOAD_KEY itself — it’s an HKDF-SHA256 derivation of PAYLOAD_KEY with a channel-scoped salt + info pair. This way the same root key can sign sessions, magic links, AND webhooks without any one of those subkeys ever colliding.
signature = HMAC-SHA256(derived_key, body) and the header value is sha256={hex(signature)}.
The Python source is awaithumans/utils/webhook_signing.py and the TS source is packages/typescript-sdk/src/adapters/temporal/index.ts (look for verifySignature). Both are short, single-purpose, and documented — the canonical place to look when porting to Go, Rust, or any other language.
Cross-language compatibility is asserted in the test suite: a body signed by Python verifies in TypeScript and vice versa.
Retry & backoff
awaithumans persists the body bytes when the task transitions and dispatches with at-least-once semantics. AWebhookDelivery row tracks the attempts.
| Attempt | Wait before this attempt |
|---|---|
| 1 | 0s (immediate) |
| 2 | 30s |
| 3 | 60s |
| 4 | 2m |
| 5 | 5m |
| 6 | 15m |
| 7 | 30m |
| 8 | 1h |
| 9 | 2h |
| 10 | 4h |
| 11 | 8h |
| 12+ | 24h, daily |
ABANDONED and an admin can redrive via the dashboard’s webhook deliveries page (or POST /api/admin/webhook-deliveries/{id}/redeliver).
The receiver only needs to be up “most of the time” — a 4-hour outage during business hours is fine; a 4-day outage requires manual redrive.
What “success” means
A 2xx response from your receiver counts as success. Anything else (4xx, 5xx, timeout, network error) is a failure and triggers retry. Your receiver should:- Verify the signature (fail with 401 if bad — but this counts as a failure and will retry; legitimate cases shouldn’t fail signature verification, so a 401 indicates real misconfiguration).
- Acknowledge fast — return 200 within 10 seconds. Long work belongs in a queue/job.
- Be idempotent — the same
task_idmay be delivered twice in extreme retry-during-outage cases. Usetask_id(oridempotency_key) as a dedup key.
What the dashboard shows
Operators can monitor the delivery queue in the dashboard at Settings → Webhook deliveries:- Pending / Succeeded / Failed (retrying) / Abandoned
- Last attempt status code & error
- Manual redrive button per row (and bulk redrive for “all abandoned in the last X days”)
Webhook abandoned so an outage that exceeds the 3-day cap doesn’t silently swallow tasks.
Headers in detail
| Header | Value | Purpose |
|---|---|---|
Content-Type | application/json | The body is a JSON object. |
X-Awaithumans-Signature | sha256=<hex> | HMAC of the raw body. Verify before parsing. |
X-Awaithumans-Task-Id | tsk_... | Convenience for log filtering — the same value is in the body. |
sha256= prefix is intentional — it future-proofs for an algorithm upgrade. Receivers should reject signatures that don’t start with that prefix.
Testing your receiver
The simplest test loop is to runawaithumans dev, hit POST /api/tasks with a callback_url pointing at your local receiver (use ngrok if needed), and complete the task from the dashboard. Your receiver gets the live signed POST.
For automated CI:
- Generate the expected signature in your test fixture using the same
PAYLOAD_KEY. - POST a synthetic body to your receiver with that signature.
- Assert your handler ran the right side effect.
Common gotchas
PAYLOAD_KEYmismatch. Both processes must use the same value. The HMAC derivation depends on it; without symmetry, signatures never verify.- Body parsing before verification. Some frameworks (Express with
express.json(), FastAPI’s auto-JSON) consume the raw body and discard it. Read the raw bytes BEFORE the body-parser fires, then verify, then parse JSON. - Sub-10s receiver budget violated. A receiver that stalls 30+ seconds gets timed out and retried — your handler may run multiple times. Acknowledge fast; defer work.
- Localhost callbacks from a remote server. If awaithumans runs on prod and your receiver is on
localhost, the POST never lands. Use ngrok for dev, a real public URL for prod. - Mixing the signing key with
PAYLOAD_KEYdirectly. They’re different —PAYLOAD_KEYis the root, the signing key is HKDF-derived. Use the helpers; don’t try to HMAC withPAYLOAD_KEYitself.
Where to next
- Temporal adapter — full workflow + receiver pattern
- LangGraph adapter — interrupt/resume in a single process, no separate receiver
- Self-hosting → Security — full crypto primitives reference