ExoCall Developer Docs

Public API & Webhooks

Integrate your CRM, website forms, or automation tools (Zapier, Pabbly Connect, Make) with ExoCall: trigger AI calls from your systems, and receive call outcomes — lead labels, summaries, transcripts — back into them the moment each call finishes.

Watch: Use the API & Webhooks for Instant Call Placement

1. Authentication

Generate an API key in Settings → API & Webhooks. The key is shown once — store it securely. Send it on every request:

Authorization: Bearer exo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Base URL: https://app.exocall.io/api/v1. All requests and responses are JSON. Keys are scoped to your workspace — you can only ever see your own data.

2. Endpoints

POST/api/v1/calls

Trigger an outbound AI call right now (speed-to-lead).

curl -X POST https://app.exocall.io/api/v1/calls \
  -H "Authorization: Bearer exo_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lead-8412" \
  -d '{
    "phone": "+919876543210",
    "contact_name": "Ravi Kumar",
    "purpose": "New enquiry from website — interested in the Growth plan. Qualify and book a demo."
  }'

# 201 Created
{ "call_sid": "abc123...", "phone": "09876543210", "status": "initiated" }

purpose is given to the AI agent as the call objective. The optional Idempotency-Key header makes retries safe — the same key within 24 hours returns the original response instead of dialing again.

GET/api/v1/calls/{'{id}'}

One call's full outcome: status, duration, AI analysis and transcript.

curl https://app.exocall.io/api/v1/calls/1234 \
  -H "Authorization: Bearer exo_live_..."

{
  "id": 1234,
  "call_type": "outbound",
  "phone": "09876543210",
  "duration_seconds": 142,
  "ended_by": "ai",
  "lead_label": "hot",            // hot | warm | cold | not_interested | callback_requested
  "lead_score": 4,                // 1–5
  "callback_datetime": null,
  "summary": "Customer asked about pricing...",
  "intent": "pricing",
  "sentiment": "positive",
  "language_used": "Hinglish",
  "key_insight": "Ready to buy if EMI option is available",
  "analysis_extra": {              // extended analysis (null on older calls)
    "call_outcome": "completed",   // completed | caller_hung_up_early | voicemail | wrong_person | dropped
    "goal_achieved": true,         // outbound calls with a purpose only
    "next_action": "Send EMI plan on WhatsApp, call back Thursday",
    "follow_up_message": "Ready-to-send WhatsApp text in the caller's language",
    "customer_details": { "name": "Ravi", "city": "Surat" },
    "competitors_mentioned": ["..."],
    "questions_unanswered": ["..."],
    "agent_performance": 4,        // 1–5 AI agent QA rating
    "agent_mistakes": null,
    "agent_talk_percent": 55
  },
  "recording_url": "https://...",
  "transcript": [ { "role": "agent", "text": "...", "spoken_at": "..." } ]
}
GET/api/v1/call-logs

Paginated call history. Filters: limit (max 100), offset, call_type (inbound/outbound), lead_label, search (phone), date_from/date_to (YYYY-MM-DD), campaign_id.

curl "https://app.exocall.io/api/v1/call-logs?lead_label=hot&limit=50" \
  -H "Authorization: Bearer exo_live_..."

{ "total": 8, "stats": { ... }, "calls": [ { "id": 1234, "lead_label": "hot", ... } ] }
POST/api/v1/leads

Push a lead in from your CRM or form. Without campaign_id the lead is called immediately; with it, the lead is added to that draft campaign instead.

curl -X POST https://app.exocall.io/api/v1/leads \
  -H "Authorization: Bearer exo_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+919876543210", "name": "Ravi", "purpose": "Follow up on demo request" }'

# 201 → { "call_sid": "...", "phone": "09876543210", "status": "call_initiated" }

# Or add to a campaign instead of calling now:
#   { "phone": "+919876543210", "name": "Ravi", "campaign_id": 12 }
# 201 → { "added_to_campaign": 12, "phone": "+919876543210", "total_leads": 341 }
POST/api/v1/campaigns/{'{id}'}/contacts

Bulk-add up to 1,000 contacts to a draft campaign in one request.

curl -X POST https://app.exocall.io/api/v1/campaigns/12/contacts \
  -H "Authorization: Bearer exo_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "contacts": [
        { "phone": "+919876543210", "name": "Ravi",  "notes": "From October webinar" },
        { "phone": "+919812345678", "name": "Meena" }
      ] }'

# 201 → { "campaign_id": 12, "added": 2, "total_leads": 343 }

3. Webhooks — call events pushed to you

Add a webhook URL in Settings → API & Webhooks and ExoCall will POST call events to it as they happen. This is how call results flow back into your CRM without polling.

call.completed

Call ended — phone, duration, who ended it.

call.analyzed

AI analysis saved (usually within ~1 minute of call end) — lead label, score, summary, intent, sentiment, callback time and full transcript. This is the event most CRM workflows should use.

ping

Sent by the "Send test event" button in Settings.

Example call.analyzed delivery:

POST <your url>
Content-Type: application/json
X-Exocall-Event: call.analyzed
X-Exocall-Signature: sha256=2f0a1b...

{
  "event": "call.analyzed",
  "created_at": "2026-07-14T10:32:00.000Z",
  "data": {
    "call_id": 1234,
    "call_type": "outbound",
    "phone": "09876543210",
    "duration_seconds": 142,
    "lead_label": "hot",
    "lead_score": 4,
    "callback_datetime": null,
    "summary": "Customer asked about pricing and EMI options...",
    "intent": "pricing",
    "sentiment": "positive",
    "language_used": "Hinglish",
    "key_insight": "Ready to buy if EMI option is available",
    "analysis_extra": { "call_outcome": "completed", "next_action": "...", "agent_performance": 4, ... },
    "recording_url": "https://...",
    "transcript": [ { "role": "agent", "text": "...", "spoken_at": "..." } ]
  }
}

Respond with any 2xx status within 10 seconds. Failed deliveries are retried after 1m, 5m, 30m and 2h; after 10 consecutive failures the endpoint is disabled until you re-enable it in Settings.

4. Verifying webhook signatures

Every delivery is signed with your endpoint's secret (shown in Settings, prefixed whsec_). Compute HMAC-SHA256 over the raw request body and compare:

// Node.js / Express example
const crypto = require('crypto');

app.post('/exocall-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.EXOCALL_WEBHOOK_SECRET)
    .update(req.body)                       // raw bytes, NOT parsed JSON
    .digest('hex');

  const received = req.get('X-Exocall-Signature') || '';
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body);
  // ... update your CRM ...
  res.status(200).end();
});

5. Errors & rate limits

All errors share one shape:

{ "error": { "code": "minutes_exhausted", "message": "Included minutes exhausted (500/500 used...)" } }
StatusCodeMeaning
401missing_api_key / invalid_api_keyNo key, malformed header, or revoked key
400invalid_phone / invalid_json / …Bad request body — the message says exactly what
402subscription_suspended / minutes_exhaustedBilling state blocks outbound calling
404not_found / campaign_not_foundResource doesn’t exist on this account
429rate_limited / concurrency_limitOver 60 req/min per key, or all call slots busy
503service_unavailableTemporary — safe to retry with backoff

Rate limit: 60 requests/minute per API key. A 429 includes a Retry-After header in seconds.

Questions or need a higher rate limit? Email info@exocall.io.