1. API key

export CALLHOOK_API_KEY=iams_live_...

Without it callhook fabricates results locally (dry-run). With it, every call is real and bills your CALL-E balance. There is no other switch.

2. Public webhook via tunnel

CALL-E POSTs terminal results to /callhook/webhook — it needs to reach you.

$ cloudflared tunnel --url http://localhost:8080
# or: ngrok http 8080

export CALLHOOK_PUBLIC_URL=https://your-tunnel.example.com

callhook automatically sets webhook_url on every call it creates.

3. Lock it down (required on any public URL)

export CALLHOOK_INTAKE_TOKEN=<random>     # ALL /api/* endpoints require: Authorization: Bearer <token>
export CALLHOOK_WEBHOOK_SECRET=<random>  # POST /callhook/webhook requires: X-Callhook-Secret: <secret>

The token gates every API endpoint (including the ones the war room uses — the Connect screen asks for it); /api/health stays open as the connect probe. Rate limiting (60 events/min per source) and CORS are built in.

4. Persistence

export CALLHOOK_JOURNAL=/var/lib/callhook/sessions.jsonl

An append-only JSONL journal. On boot, callhook replays it: sessions, armed retries, schedules and dedup keys all survive restarts — a crash mid-retry-cycle never silently drops or double-dials a customer.

Configuration reference

VariableDefaultMeaning
CALLHOOK_API_KEYCALL-E API key. Empty = dry-run mode.
CALLHOOK_API_BASEhttps://api.heycall-e.comAPI base override (e.g. a proxy).
CALLHOOK_PUBLIC_URLPublic URL CALL-E uses to deliver terminal webhooks.
CALLHOOK_ADDR:8080HTTP listen address.
CALLHOOK_INTAKE_TOKENBearer token for event intake.
CALLHOOK_WEBHOOK_SECRETShared secret for the CALL-E webhook.
CALLHOOK_JOURNALdata/sessions.jsonlSession journal path.
CALLHOOK_ENFORCE_WINDOWStrueDefer calls outside polite local hours.
CALLHOOK_RETRY_DELAY2hRedial delay after no_answer.
CALLHOOK_RETRY_TICK15sScheduler poll interval.

Calling behavior defaults

Polite hours

9:00–20:00 recipient-local, weekdays only. Per-region timezone map; override per event with tz. Deferrals never consume retry attempts.

Retry ladder

no_answer and transient failures redial up to 2 times. Blocked, invalid, and out-of-region numbers are never redialed. Refusals are terminal — answered-and-said-no is final.

One attempt = one idempotency key

Each dial attempt gets its own key at CALL-E — a crash between "redial armed" and "redial placed" can never produce two calls.

Connecting real business data

The demo ships a mock store. Production implements one interface — business.Store: GetCustomer, GetOpenInvoice, MarkPromise, Escalate, RecordContact — against your CRM or billing system. Read tools feed the call context; writes land only from structured outcomes, policy-gated and audit-logged.