Developer Reference

Phone Webhook API Reference

Connect any phone system to Closer Mode using a single POST request. Supports Twilio, Salesforce, 360CTI, or any custom dialer — with optional inline transcripts and metadata passthrough.

Real-time scoring
Bearer token auth
Any phone provider

Endpoint

Each integration gets a unique webhook URL generated at setup time. The URL contains a per-integration token that acts as the first layer of authentication.

http
POST https://app.closermode.ai/api/webhooks/phone/generic/{token}

Content-Type: application/json
Authorization: Bearer {api_key}
Where to find your URL and key: In the Closer Mode dashboard, go to Settings → Phone Providers, open your Custom integration, and copy the webhook URL. Generate an API key from the same card if you haven't already.

Authentication

Send your API key as a Bearer token in the Authorization header:

http
Authorization: Bearer cm_live_a1b2c3d4e5f6...

The key is verified using a constant-time SHA-256 comparison — plaintext is never stored. If your key is compromised, regenerate it from the dashboard.

Security Layers

Layer 1Path token

The webhook URL contains a unique UUID token per integration. Guessing it is computationally infeasible.

Layer 2Bearer API key

SHA-256 hashed, verified with constant-time comparison to prevent timing attacks. Shown once at creation.

Layer 3IP allowlist (optional)

Restrict inbound requests to specific IP ranges in your integration settings.

Request Body

Send a JSON payload. Only externalId is required — all other fields are optional but improve scoring quality.

FieldTypeStatusDescription
externalIdstringrequiredYour system's unique call ID. Used for idempotency — re-sending the same externalId is safe and returns the existing call.
repNamestringoptionalDisplay name of the sales rep. Used in call titles and participant lists.
callerPhonestringoptionalCaller's phone number in E.164 format (e.g. +15551234567).
durationnumberoptionalCall duration in seconds.
callDatestringoptionalISO 8601 datetime of when the call occurred. Defaults to current time if omitted.
recordingUrlstring (url)optionalDirect URL to the audio recording. Closer Mode will download and transcribe it if no transcript is provided.
transcriptstringoptionalPre-built transcript text. When provided, audio transcription is skipped — fastest path to scoring.
metadataobjectoptionalArbitrary key-value pairs stored alongside the call. Useful for CRM IDs, lead IDs, campaign tags, etc.

Call Filtering

To avoid scoring incomplete or irrelevant calls, Closer Mode applies these rules before creating a call record:

externalId is present and non-empty
At least one of: recordingUrl or transcript is provided
externalId already exists for this integration (idempotent skip, HTTP 200)

Filtered calls receive { "skipped": true, "reason": "..." } with HTTP 200. Your phone system can safely ignore these.

Recording URLs

Direct URLs (recommended)

Twilio, smrtPhone, and most dialers provide public recording URLs. Closer Mode fetches these directly — no extra config needed.

https://api.twilio.com/recordings/RE...
Auth-gated URLs

Salesforce-hosted recordings require OAuth and cannot be fetched directly. Send a transcript instead to bypass this limitation.

Pass transcript field directly ↓

Inline Transcripts

The fastest path to scoring: include the transcript directly in the payload. Closer Mode skips transcription entirely and goes straight to AI scoring.

Transcript strategy (in order of preference)

  1. transcript field provided → use directly, skip audio download
  2. recordingUrl provided → download audio and transcribe via AssemblyAI
  3. Neither provided → call is skipped (no recording)

Transcript format: plain text, speaker turns separated by newlines. Label speakers as Agent: / Customer: for best scoring accuracy.

Metadata Passthrough

The metadata object is stored verbatim alongside the call and is available in exports and downstream webhooks. Use it to attach CRM record IDs, campaign tags, or any other context your team needs.

json
{
  "externalId": "call-abc123",
  "metadata": {
    "leadId": "lead-456",
    "callLogId": "log-789",
    "campaignId": "spring-2026",
    "source": "360CTI",
    "disposition": "Interested"
  }
}

Examples

Minimal payload

bash
curl -X POST \
  "https://app.closermode.ai/api/webhooks/phone/generic/{token}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {api_key}" \
  -d '{
    "externalId": "call-20260424-001",
    "recordingUrl": "https://example.com/recordings/call-001.mp3"
  }'

Full payload with transcript

bash
curl -X POST \
  "https://app.closermode.ai/api/webhooks/phone/generic/{token}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {api_key}" \
  -d '{
    "externalId": "call-20260424-002",
    "repName": "Jane Smith",
    "callerPhone": "+15551234567",
    "duration": 245,
    "callDate": "2026-04-24T10:00:00Z",
    "transcript": "Agent: Hi, this is Jane. How can I help you?\nCustomer: I saw your listing...",
    "metadata": {
      "leadId": "lead-999",
      "callLogId": "log-20260424",
      "source": "360CTI"
    }
  }'

Node.js

javascript
const response = await fetch(
  "https://app.closermode.ai/api/webhooks/phone/generic/{token}",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer {api_key}",
    },
    body: JSON.stringify({
      externalId: call.id,
      repName: call.agentName,
      callerPhone: call.fromNumber,
      duration: call.durationSeconds,
      callDate: call.startedAt,
      recordingUrl: call.recordingUrl,
      metadata: {
        leadId: call.leadId,
        source: "my-dialer",
      },
    }),
  }
);

const result = await response.json();
// { success: true, callId: "uuid..." }

Responses

200 — Created
{ "success": true, "callId": "9db7cebd-e628-4415-948c-11ede6187636" }

Call was created and queued for AI scoring.

200 — Skipped
{ "skipped": true, "reason": "duplicate" }

Duplicate externalId or no recording/transcript. Safe to ignore.

401 — Unauthorized
{ "error": "Unauthorized" }

Invalid or missing Bearer token, or wrong webhook URL.

400 — Bad Request
{ "error": "Invalid payload: externalId: Required" }

Payload failed schema validation.

Setup Checklist

1In Closer Mode: Settings → Phone Providers → Add Integration → Custom / Generic
2Give the integration a name (e.g. "360CTI") and click Create Integration
3Copy the Webhook URL and API key — the key is shown only once
4Configure your phone system to POST to the webhook URL after each call
5Set the Authorization: Bearer header using the API key
6Send a test payload and verify the call appears in your Closer Mode dashboard
7Optionally: set an IP allowlist in the integration settings for extra security

Ready to connect?

Log in to Closer Mode and add your first Custom integration in under 2 minutes.

Open Integration Settings