The war room won't connect
| Symptom | Cause & fix |
| "could not reach server" | Server not running, or wrong URL. Start it: cd backend && go run ./cmd/callhook (or the released binary) and connect to http://localhost:8080. → Quickstart |
| "invalid or missing bearer token" | The server has CALLHOOK_INTAKE_TOKEN set; paste the same token on the Connect screen. → Production → Lock it down |
| Connects, but "DRY-RUN" badge everywhere | Not an error — no CALLHOOK_API_KEY means fabricated calls, zero cost, full pipeline. → Going live |
Platform webhook returns 401
Every adapter verifies the platform's own signature. A 401 body looks like {"error":"STRIPE_WEBHOOK_SECRET not configured","docs":"https://callhook.github.io/..."} — the docs field is the fix.
| Error | Fix |
<ENV_VAR> not configured | Set the adapter's secret (e.g. STRIPE_WEBHOOK_SECRET=whsec_...) and restart. Per-platform steps: Native adapters + integrations/<platform>/README.md in the repo. |
invalid ... signature | The secret doesn't match what the platform sends — re-copy it from the platform's webhook settings, not the API key. Watch hex vs base64: Shopify/WooCommerce/Xero/Typeform are base64; GitHub/Stripe/Calendly/Paddle are hex. |
| HubSpot: correct secret, still invalid | Behind a tunnel the signed URI is the public one. Set HUBSPOT_EXTERNAL_BASE_URL=https://your-tunnel.example.com and restart. → Production |
| Stripe/Slack worked before, now 401 | Replay window: signatures older than 5 minutes are rejected. Sync your clock (NTP). Secret rotation: during the overlap both signatures are accepted only if both secrets are valid — v1 lists support this; otherwise wait out the window. |
| SNS: "cert url host must be *.amazonaws.com" | SigningCertURL failed validation — message tampered or misconfigured. Also confirm the subscription (SNS_AUTO_SUBSCRIBE=true or click the SubscribeURL in the delivery logs). |
| Chargebee/Pipedrive: 401 with correct credentials | They use HTTP Basic auth, not HMAC. The username/password must be the ones configured in their webhook settings screens — not API keys. |
POST /api/events errors
| Error | Fix |
type is required / customer_id is required / id is required | All three are mandatory fields. → API → POST /api/events |
unsupported event type | The response lists supported_types — use one of the 8 blueprints. → Event Types |
customer ... not found | Demo store knows cus_1001–cus_1003 and cus_2001–cus_2030. For real data implement business.Store. → Store adapters |
phone must be E.164 format | Numbers start with + and country code, e.g. +14155550002. → API |
{"status":"duplicate"} | Not an error — the event id was seen before (idempotency). New call → new id. |
{"status":"deferred"} | Not an error — outside calling hours (9:00–20:00 recipient-local, weekdays); dials automatically at the next window. |
prefetch invoice: no open invoice | The blueprint's Compose step needs data the store doesn't have — an invoice.due needs an open invoice for that customer. → Blueprint contract |
not_before must be RFC3339 | Use ISO-8601/RFC3339 like 2026-09-08T09:00:00Z. → API |
Events fire, but no calls happen
| Check | How |
| Dry-run mode? | GET /api/health → dry_run: true means calls are fabricated on purpose (no balance spent) — sessions still complete. → Going live |
| Outside calling hours? | Sessions show call_deferred in the audit trail; they dial at the next window. For testing: CALLHOOK_ENFORCE_WINDOWS=false. |
Real mode + call_failed? | CALL-E rejected the call — check CALLHOOK_API_KEY and CALLHOOK_PUBLIC_URL (CALL-E must reach /callhook/webhook). → Production |
Campaign problems
| Symptom | Cause & fix |
audience is empty | all_overdue found no customers with open invoices. Pass an explicit audience list, or fix store data. → Campaigns |
goal.target must be > 0 | Count goals need a target. → Create a campaign |
| Running, but no waves launch | Wave delay hasn't elapsed (Go duration: "15m"). The pump ticks every CALLHOOK_RETRY_TICK (default 15s). |
| Everyone marked skipped | The goal was met — early-stop is the feature. → Lifecycle & early-stop |
Install / startup problems
| Symptom | Fix |
port :8080 already in use | CALLHOOK_ADDR=:9090; connect the war room to the new port. |
| Journal replay errors on boot | data/sessions.jsonl is append-only; corrupt tail lines are skipped. Delete only if history doesn't matter. → Persistence |
go: go.mod file not found | Run from backend/ (module root), not the repo root. |
| Dashboard 404 / blank page | Binary built without the web app — make full in backend/ embeds it, or use the release binary. → Install |
Where everything lives