Webhooks

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.

Subscribing

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

Delivery shape

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.

Signature verification

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>

Event catalog

All 15 event types, with the shape of data.

Call lifecycle

EventWhendata highlights
call.startedPipeline enters IN_PROGRESScall_id, agent_id, direction, from, to
call.completedPipeline ends successfully+ duration_ms, outcome, recording_url, summary, cost_usd
call.failedPipeline terminates in error+ error_code, error_message
call.hangupEither side hangs up mid-call+ end_reason
call.transferredTransfer tool invoked by the agent+ transfer_to, transfer_type (warm/cold)

Post-call artifacts

EventWhendata highlights
transcript.readyTranscript persisted after post-call+ turns_count, transcript_url
recording.readyMP3 uploaded to storage+ recording_url (signed, short-lived), duration_ms
analysis.readyPost-call analysis complete+ outcome, summary, scorecard, tool_executions

Campaign lifecycle

EventWhendata highlights
campaign.startedCampaign dispatched its first contactcampaign_id, total_contacts
campaign.completedAll campaign contacts finished+ success_count, failure_count, no_answer_count
campaign.contact.completedPer-contact call ended+ contact_id, call_id, outcome

Agent state

EventWhendata highlights
agent.activatedAgent kill switch toggled back onagent_id
agent.deactivatedAgent kill switch toggled offagent_id

Billing / usage

EventWhendata highlights
usage.credit_exhaustedFree-tier or prepay credit at zerotenant_id, last_call_id
usage.daily_cap_approachingDaily-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"

Delivery history + replay

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"

Test fire

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.

Troubleshooting

  • 429 from your endpoint → CallingScout retries with backoff. Plan the handler to be idempotent; dedupe by event_id.
  • Signature mismatch → usually caused by body mutation by a middleware (whitespace, re-serialization). Verify against the raw request bytes you received on the wire.
  • Old timestamp → clock drift on your receiver; compare against time.time() with a tolerance.
  • Cross-mode confusion → sandbox-flagged subs don't see prod events and vice versa. Confirm sandbox on both the subscription and the API key that fired the work.