Call Analytics API documentation

Webhooks

Webhooks work in both directions. Inbound, you push calls to us from any platform. Outbound, we push signed lifecycle events — including the finished report — to your endpoints.

Inbound: push a call to us

POSThttps://api.callanalyticsapi.com/webhooks/inbound/generic

A single, flexible endpoint that turns almost any platform's webhook into a call. Authenticate with your API key in the X-Api-Key header (or Authorization: Bearer) — the key both identifies your company and acts as the shared secret.

Flexible field mapping

The endpoint accepts several common field names for each value, so you can often point a platform's native webhook straight at it. For each call attribute, the first present key wins:

Call attributeAccepted JSON keys (in priority order)
External idexternal_call_id, call_id, id
Rep namerep_name, agent_name, user_name
Rep emailrep_email, agent_email
Customer namecustomer_name, contact_name
Customer phonecustomer_phone, phone, from
Customer emailcustomer_email, email
Recording URLrecording_url, recording, audio_url
Duration (sec)duration_seconds, duration
Started atcall_started_at, started_at
CRM contact idcrm_contact_id, contact_id
CRM deal idcrm_deal_id, deal_id

The entire raw payload is stored alongside the call, so anything not mapped is still retained.

Request

POST https://api.callanalyticsapi.com/webhooks/inbound/generic
X-Api-Key: cc_3a91f8e2_your_api_key
Content-Type: application/json

{
  "id": "ghl-evt-7781",
  "contact_name": "Acme Corp",
  "phone": "+15125550199",
  "recording": "https://storage.example.com/rec/7781.mp3",
  "duration": 1560,
  "agent_name": "Jordan Lee"
}

Responses

  • 201 {"call_id":4131,"status":"received"} — a new call was created.
  • 200 {"call_id":4131,"status":"duplicate"} — the external id already exists; the existing call is returned. Dedupe is keyed on the external id, so the endpoint is safe to retry.
  • 401 {"error":"unauthorized",...} — missing or invalid key/secret.
  • 422 {"error":"empty_payload"} — the JSON body was empty.

Outbound: receive events from us

Add an endpoint under Company → Webhooks in the app and subscribe it to the events you care about. When an event fires, we POST a JSON body to your URL and sign it.

Event types

EventFires when
call.receivedA call was accepted and queued.
call.processing_startedThe pipeline began working the call.
call.transcription_completedTranscription finished.
call.analysis_completedAI analysis finished.
call.report_completedThe structured report is ready (payload includes the report).
call.failedProcessing failed for the call.
call.low_score_detectedThe call scored below your threshold.
call.buying_signal_detectedA buying signal was detected.
call.compliance_alert_detectedA compliance issue was flagged.

Signature

Every delivery carries an HMAC-SHA256 signature in the X-Cadence-Signature header, computed over timestamp + "." + raw_body using your endpoint's signing secret (shown once when you create the endpoint):

X-Cadence-Signature: t=1718291040,v1=4f8c…e21a
X-Cadence-Event: call.report_completed
Content-Type: application/json

To verify: split the header on ,, read t and v1, recompute HMAC_SHA256(secret, t + "." + body), and compare in constant time. Reject deliveries whose timestamp is outside a tolerance window (we use 300 seconds) to prevent replay.

PHP

<?php
function verify(string $body, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $kv) {
        [$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
        $parts[$k] = $v;
    }
    $ts = (int) ($parts['t'] ?? 0);
    if ($ts === 0 || abs(time() - $ts) > $tolerance) return false;
    $expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
    return isset($parts['v1']) && hash_equals($expected, $parts['v1']);
}

$body = file_get_contents('php://input');
if (!verify($body, $_SERVER['HTTP_X_CADENCE_SIGNATURE'] ?? '', getenv('CADENCE_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);

Node.js

const crypto = require('crypto');

function verify(rawBody, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')));
  const ts = parseInt(parts.t || '0', 10);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`).digest('hex');
  return parts.v1 &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

// express, with express.raw({ type: 'application/json' })
app.post('/cadence', (req, res) => {
  const ok = verify(req.body.toString('utf8'),
    req.get('X-Cadence-Signature') || '', process.env.CADENCE_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200);
});

Respond 2xx to acknowledge. Non-2xx responses are recorded as failed deliveries and retried. You can inspect recent deliveries via GET /v1/webhooks/events.

Recent deliveries

GET/v1/webhooks/events
{
  "data": [
    { "id": 9912, "event_type": "call.report_completed", "status": "delivered",
      "response_status": 200, "attempts": 1, "created_at": "2026-06-22T15:22:10Z" }
  ]
}