CallingScout fires outbound webhooks for every meaningful lifecycle event — call lifecycle, post-call artifacts, campaign progress, agent state, usage. One subscription (tenant + URL) receives every event type it opts into.
curl https://api.callingscout.ai/api/v1/webhooks \
-H "Authorization: Bearer $CALLINGSCOUT_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/hooks/callingscout",
"event_types": ["*"],
"sandbox": false
}'
The response carries the signing secret exactly once — store it in a secret manager. event_types accepts "*" (all) or a concrete list. sandbox: true subscribers only receive events from sk_test_ API-key calls; sandbox: false subscribers only receive production events. There's no cross-bleed.
Manage subscriptions: GET/PATCH/DELETE /api/v1/webhooks/{id}.
POST /hooks/callingscout HTTP/1.1
Content-Type: application/json
X-CallingScout-Event: call.completed
X-CallingScout-Event-Id: 7a1f5c42-…
X-CallingScout-Signature: t=1714000000,v1=9f0a2b…
{
"event_id": "7a1f5c42-…",
"event_type": "call.completed",
"tenant_id": "…",
"sandbox": false,
"occurred_at": 1714000000,
"data": { /* event-specific payload */ }
}
At-least-once delivery. Dedupe on event_id. Retry with exponential backoff if we get a non-2xx response (or any network error); we stop after 10 attempts spanning 24 hours.
The signature header is Stripe-pattern:
X-CallingScout-Signature: t=<unix_ts>,v1=<hex_hmac>
hmac = HMAC-SHA256(secret, f"{t}." + raw_body_bytes). Reject if abs(now - t) > 300 (5 min default) to prevent replay.
Verify manually in your preferred language:
<details><summary>Python</summary>import hmac, hashlib, time
def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(int(time.time()) - int(parts["t"])) > tolerance:
return False
signed = f"{parts['t']}.".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
</details>
<details><summary>TypeScript (Node)</summary>
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(
body: Buffer,
header: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2) as [string, string]),
);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(parts.t)) > toleranceSec) return false;
const signed = Buffer.concat([Buffer.from(`${parts.t}.`), body]);
const expected = createHmac("sha256", secret).update(signed).digest("hex");
return expected.length === parts.v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
</details>
All 15 event types, with the shape of data.
| Event | When | data highlights |
|---|---|---|
call.started | Pipeline enters IN_PROGRESS | call_id, agent_id, direction, from, to |
call.completed | Pipeline ends successfully | + duration_ms, outcome, recording_url, summary, cost_usd |
call.failed | Pipeline terminates in error | + error_code, error_message |
call.hangup | Either side hangs up mid-call | + end_reason |
call.transferred | Transfer tool invoked by the agent | + transfer_to, transfer_type (warm/cold) |
| Event | When | data highlights |
|---|---|---|
transcript.ready | Transcript persisted after post-call | + turns_count, transcript_url |
recording.ready | MP3 uploaded to storage | + recording_url (signed, short-lived), duration_ms |
analysis.ready | Post-call analysis complete | + outcome, summary, scorecard, tool_executions |
| Event | When | data highlights |
|---|---|---|
campaign.started | Campaign dispatched its first contact | campaign_id, total_contacts |
campaign.completed | All campaign contacts finished | + success_count, failure_count, no_answer_count |
campaign.contact.completed | Per-contact call ended | + contact_id, call_id, outcome |
| Event | When | data highlights |
|---|---|---|
agent.activated | Agent kill switch toggled back on | agent_id |
agent.deactivated | Agent kill switch toggled off | agent_id |
| Event | When | data highlights |
|---|---|---|
usage.credit_exhausted | Free-tier or prepay credit at zero | tenant_id, last_call_id |
usage.daily_cap_approaching | Daily-spend limit hit 80% | spent_usd, limit_usd |
You can also fetch the catalog programmatically:
curl https://api.callingscout.ai/api/v1/webhooks/event-types \
-H "Authorization: Bearer $CALLINGSCOUT_KEY"
We keep the last 30 days of delivery attempts. Inspect them:
# Last 50 attempts for a subscription
curl "https://api.callingscout.ai/api/v1/webhooks/{id}/deliveries?limit=50" \
-H "Authorization: Bearer $CALLINGSCOUT_KEY"
Each row has event_id, event_type, url, response_status, attempt, status (pending / succeeded / failed / skipped), error, created_at, delivered_at.
Replay a specific delivery (creates a new attempt row referring to the same event_id, preserving audit trail):
curl -X POST https://api.callingscout.ai/api/v1/webhooks/deliveries/{delivery_id}/replay \
-H "Authorization: Bearer $CALLINGSCOUT_KEY"
Push a synthetic event to your endpoint to verify your signature verification + handler without waiting for a real call:
curl -X POST https://api.callingscout.ai/api/v1/webhooks/{id}/test \
-H "Authorization: Bearer $CALLINGSCOUT_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "call.completed",
"payload": {"from_ci": true}
}'
Returns the delivery row so you can see whether your endpoint responded 2xx.
event_id.time.time() with a tolerance.sandbox on both the subscription and the API key that fired the work.