EpiCall AI API
Start phone campaigns, add contacts, get call results and transcripts, and put chat and voice agents in your own website, app or server. Every request is HTTPS with JSON bodies.
Quick start
- Sign in to the console and open Administration → Deploy Keys. No account yet? Start a free trial.
- Create a server key. It is shown once, so store it safely.
- Send it as a Bearer token. This lists your agents:
https://epicall.ai/console/api/v1export API_KEY="ek_sk_…" # your server key
curl https://epicall.ai/console/api/v1/agents \
-H "Authorization: Bearer $API_KEY"Authentication
Send your key in the Authorization header on every request:
Authorization: Bearer <your key>| Key | Starts with | Use it from | Notes |
|---|---|---|---|
| Server key | ek_sk_ | Your backend only | Full access to your workspace: campaigns, call logs, chat. Never put it in a web page or mobile app. |
| Website key | ek_pk_ | Web pages | Works only from the website domains you list on the key. Used by the voice and chat widgets. |
- Keys are created and revoked in Deploy Keys. Revoking takes effect immediately.
- A key works only on the console domain it was created on (epicall.ai for EpiCall AI).
- Endpoints marked Server key refuse website keys with
403 secret_key_required.
Errors
Errors return an HTTP status and a JSON body with a stable code:
{ "error": { "code": "invalid_key", "message": "That key is not valid, or has been revoked." } }| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key | No Authorization: Bearer header |
| 401 | invalid_key | Unknown or revoked key, or a key from another domain |
| 403 | secret_key_required | A website key was sent to a server-key endpoint |
| 403 | origin_not_allowed | A website key was used from a domain not listed on it |
| 402 | subscription_inactive | The trial ended or the subscription is not active |
| 402 | quota_exceeded | The plan's voice minutes or conversations are used up |
| 429 | rate_limited | Too many requests; try again in a minute |
Each endpoint below lists its own additional errors.
Rate limits
Limits are per key, over a rolling 60 seconds.
| Endpoints | Requests per minute |
|---|---|
| Agents, call logs, campaigns, contacts, voice sessions | 60 |
| Chat messages, history, config, handover | 1,200 (40 per visitor IP with a website key) |
| Chat updates (polling) | 6,000 (30 per visitor IP with a website key) |
Agents
/agentsServer keyLists your voice agents. Use an agent's id as agent_id elsewhere.
curl https://epicall.ai/console/api/v1/agents \
-H "Authorization: Bearer $API_KEY"{ "data": [ { "id": "support_agent", "name": "Support Agent", "languages": ["en", "hi"] } ] }Call logs
/callsServer keyYour calls from every channel (phone campaigns, website widget, WhatsApp, app), newest first.
| Query | Type | Required | Notes |
|---|---|---|---|
agent_id | string | No | Only this agent's calls |
from | date or date-time | No | First day, e.g. 2026-09-01 (India time), or an exact ISO date-time |
to | date or date-time | No | Last day, included, e.g. 2026-09-30, or an exact ISO date-time |
tz | IANA time zone | No | Zone for from/to days, e.g. Europe/London. Default Asia/Kolkata |
days | integer | No | Only the last N days (instead of from/to) |
limit | integer | No | 1–200, default 50 |
offset | integer | No | Calls to skip, for paging: offset=200 with limit=200 is the second page. Default 0 |
curl "https://epicall.ai/console/api/v1/calls?from=2026-09-01&to=2026-09-30&agent_id=support_agent&limit=200&offset=0" \
-H "Authorization: Bearer $API_KEY"{ "total": 1234, "limit": 200, "offset": 0, "data": [ {
"id": "sess_abc123", "agent_id": "support_agent", "agent_name": "Support Agent",
"started_at": "2026-09-29T10:15:02.000Z", "duration_seconds": 184,
"cost_usd": 0.42, "tool_calls": 3, "avg_response_seconds": 0.9, "has_details": true
} ] }total is how many calls match in all. Keep adding limit to offset until you have them all.
Errors: 400 invalid_range when from or to can't be read, or from is after to.
/calls/{id}Server keyOne call with its transcript. caller_ref is the reference you (or a campaign) attached to the call.
curl https://epicall.ai/console/api/v1/calls/sess_abc123 \
-H "Authorization: Bearer $API_KEY"{
"id": "sess_abc123", "agent_id": "support_agent",
"started_at": "2026-09-29T10:15:02.000Z", "ended_at": "2026-09-29T10:18:06.000Z",
"duration_seconds": 184, "cost_usd": 0.42, "caller_ref": "order-1001",
"transcript": [
{ "type": "speech", "speaker": "agent", "text": "Hi, this is Priya from Acme…", "at": "…" },
{ "type": "speech", "speaker": "caller", "text": "Yes, go ahead.", "at": "…" },
{ "type": "tool_call", "tool": "hubspot_lookup_contact", "duration_ms": 412, "at": "…" },
{ "type": "language_switch", "language": "hi", "at": "…" }
]
}Transcript entry types: speech, tool_call, language_switch, reengage, call_ended_idle. Errors: 404 call_not_found.
Campaigns
A campaign is an agent calling a list of people. You create it as a draft, add contacts, then launch it. It calls within the days and hours you set, retries people who don't answer, and analyses every call for an outcome and whether the goal was met.
/campaignsServer keyCreates a campaign in draft, optionally with up to 5,000 contacts. No one is called until you launch it.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Up to 120 characters |
agent_id | string | Yes | The agent that makes the calls |
channel | "phone" | "whatsapp_message" | No | Default phone. whatsapp_message sends an approved WhatsApp template instead of calling |
goal | string | No | What counts as success, e.g. “Book a demo”. Used to judge goal_achieved. Up to 500 characters |
agent_b_id | string | No | A second agent for an A/B test; contacts alternate between the two |
timezone | IANA name | No | Default Asia/Kolkata |
days | integer[] | No | Days to call, 0 = Sunday. Default [1,2,3,4,5,6] (Mon–Sat) |
window_start / window_end | "HH:MM" | No | Daily calling hours in the campaign's timezone. Default 10:00–19:00 |
start_at / end_at | ISO date-time | No | Optional start and finish. Reaching end_at completes the campaign |
max_concurrent | integer | No | Calls at the same time, 1–20. Default 2 |
calls_per_minute | integer | No | New calls per minute, 1–60. Default 4 |
max_attempts | integer | No | Tries per person, 1–5. Default 3 |
retry_after_min | integer | No | Minutes before retrying a no-answer, 10–10080. Default 60 |
personalize | boolean | No | The agent greets each person by name and uses their fields. Default false |
webhook_url | https URL | No | Receives a POST for every finished contact and when the campaign completes. See Webhooks |
followup_on | outcome[] | No | Send a WhatsApp template after these outcomes (needs wa_phone_number_id and wa_template) |
wa_phone_number_id / wa_template / wa_language | string | No | Your connected WhatsApp number and approved template, for whatsapp_message campaigns and follow-ups |
contacts | Contact[] | No | Up to 5,000 here; add more later. See Campaign contacts |
curl -X POST https://epicall.ai/console/api/v1/campaigns \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "October renewals",
"agent_id": "sales_agent",
"goal": "Book a renewal call",
"days": [1,2,3,4,5], "window_start": "10:00", "window_end": "18:30",
"max_concurrent": 3, "calls_per_minute": 6,
"max_attempts": 3, "retry_after_min": 120,
"personalize": true,
"webhook_url": "https://example.com/hooks/calls",
"contacts": [
{ "phone": "+919876543210", "name": "Priya Sharma", "fields": { "plan": "Pro", "renewal_date": "2026-10-15" } }
]
}'{ "id": 12, "status": "draft",
"imported": { "added": 1, "duplicates": 0, "invalid": 0, "do_not_call": 0, "invalid_samples": [] } }Errors: 400 invalid_body, 422 invalid_settings (the message says which setting), 422 invalid_contacts.
/campaigns/{id}Server keyStarts, pauses, resumes or stops a campaign. Body: { "action": "launch" | "pause" | "resume" | "stop" }
| Action | Allowed when | Result |
|---|---|---|
launch | draft, with at least one contact | running, or scheduled if start_at is in the future. More than 500 contacts: pending_approval until an owner approves it in the console |
pause | running or scheduled | paused. Calls already in progress finish normally |
resume | paused | running (still only within the calling hours) |
stop | running, scheduled, paused or pending_approval | stopped. Everyone not yet called is skipped. Final |
curl -X PATCH https://epicall.ai/console/api/v1/campaigns/12 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"action":"launch"}'{ "id": 12, "status": "running" }Errors: 404 not_found, 422 invalid_action, 409 not_allowed (e.g. “Only a draft can be launched.”).
/campaignsServer keyYour latest 200 campaigns, newest first, each with its settings and live stats.
curl https://epicall.ai/console/api/v1/campaigns \
-H "Authorization: Bearer $API_KEY"/campaigns/{id}Server keyOne campaign with its settings and live stats.
{ "id": 12, "name": "October renewals", "channel": "phone", "status": "running",
"agent_id": "sales_agent", "agent_b_id": null, "goal": "Book a renewal call",
"schedule": { "timezone": "Asia/Kolkata", "start_at": null, "end_at": null,
"days": [1,2,3,4,5], "window_start": "10:00", "window_end": "18:30" },
"pacing": { "max_concurrent": 3, "calls_per_minute": 6 },
"retries": { "max_attempts": 3, "retry_after_min": 120 },
"personalize": true, "webhook_url": "https://example.com/hooks/calls",
"created_at": "…", "launched_at": "…", "finished_at": null,
"stats": { "contacts": 800, "dialled": 310, "connected": 190, "goals_achieved": 42,
"attempts": 402, "in_call": 2, "avg_duration_s": 95,
"by_status": { "pending": 480, "done": 300, "retry": 20 },
"by_outcome": { "interested": 42, "no_answer": 80 } } }Campaign contacts
/campaigns/{id}/contactsServer keyAdds up to 5,000 contacts per request, up to 20,000 per campaign. Works on a draft, scheduled, running or paused campaign; a running one calls new people as their turn comes.
| Field | Type | Required | Notes |
|---|---|---|---|
contacts[].phone | string | Yes | Indian mobile number: +919876543210, 919876543210 or 9876543210 |
contacts[].name | string | No | Up to 120 characters |
contacts[].fields | object | No | Up to 30 key/value pairs (e.g. plan, due_date) the agent can use, and WhatsApp template variables |
curl -X POST https://epicall.ai/console/api/v1/campaigns/12/contacts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts":[{"phone":"98765 43210","name":"Rahul","fields":{"city":"Pune"}}]}'{ "added": 1, "duplicates": 0, "invalid": 0, "do_not_call": 0, "invalid_samples": [] }- Duplicates (in the request or already in the campaign) are counted and skipped.
- Numbers on your do-not-call list are skipped. Anyone who asks not to be called is added to it automatically.
- Errors:
404 not_found,409 finished,422 invalid_contacts,422 too_many.
/campaigns/{id}/contactsServer keyPer-person results, most recently updated first.
| Query | Type | Required | Notes |
|---|---|---|---|
status | string | No | pending, calling, retry, callback, done, failed or skipped |
outcome | string | No | See Statuses and outcomes |
limit | integer | No | Up to 1,000. Default 100 |
offset | integer | No | For paging. Default 0 |
curl "https://epicall.ai/console/api/v1/campaigns/12/contacts?status=done&limit=500" \
-H "Authorization: Bearer $API_KEY"{ "total": 300, "contacts": [ {
"id": 345, "phone": "+919876543210", "name": "Priya Sharma", "fields": { "plan": "Pro" },
"status": "done", "attempts": 1, "outcome": "interested", "outcome_label": "Interested",
"goal_achieved": true, "summary": "Agreed to renew; wants the invoice by email.",
"details": { "email": "priya@example.com", "sentiment": "positive" },
"duration_s": 132, "next_attempt_at": null, "updated_at": "2026-09-29T10:20:00.000Z"
} ] }Calling one person
To have an agent call a single person from your system (a new lead, a missed payment, an appointment reminder), create a campaign with that one contact and launch it. Set the calling hours to suit you; the call goes out within a few seconds when inside them.
# 1. Create the campaign with one contact
curl -X POST https://epicall.ai/console/api/v1/campaigns \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Lead 7781 follow-up",
"agent_id": "sales_agent",
"goal": "Book a demo",
"days": [0,1,2,3,4,5,6], "window_start": "09:00", "window_end": "21:00",
"max_attempts": 2, "retry_after_min": 30,
"personalize": true,
"webhook_url": "https://example.com/hooks/calls",
"contacts": [ { "phone": "+919876543210", "name": "Amit", "fields": { "source": "website form" } } ]
}'
# 2. Launch it (use the id from step 1)
curl -X PATCH https://epicall.ai/console/api/v1/campaigns/12 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"action":"launch"}'The result arrives at your webhook_url as contact.completed, or poll GET /campaigns/12/contacts.
Webhooks
Set webhook_url on a campaign to receive a POST when each person's calls are finished, and when the whole campaign completes.
| Event | Sent when |
|---|---|
contact.completed | A contact reaches done or failed (after all retries) |
campaign.completed | Everyone has been called, or end_at is reached. Not sent when you stop a campaign |
X-EpiCall-Event: contact.completed
X-EpiCall-Signature: sha256=5d41402abc4b2a76b9719d911017c592…
Content-Type: application/json
{ "event": "contact.completed", "sent_at": "2026-09-29T10:20:01.000Z",
"campaign": { "id": 12, "name": "October renewals", "status": "running" },
"contact": { "phone": "+919876543210", "name": "Priya Sharma", "fields": { "plan": "Pro" },
"outcome": "interested", "outcome_label": "Interested", "goal_achieved": true,
"summary": "Agreed to renew; wants the invoice by email.",
"details": { "sentiment": "positive", "callback_at": null },
"duration_s": 132, "attempts": 1 } }Verify the signature
The signing secret (whsec_…) is on the campaign's page in the console. Compute an HMAC-SHA256 of the raw request body with it and compare:
import crypto from "node:crypto";
function isGenuineWebhook(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(signatureHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hmac, hashlib
def is_genuine_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")Respond with any 2xx within 5 seconds. Deliveries are not retried, so poll GET /campaigns/{id}/contacts if you need to catch up.
Voice sessions
Talk to an agent in real time from a browser, an app, or your own phone system.
/call-tokensServer or website keyReturns a single-use WebSocket URL, valid for 60 seconds, for one voice session with an agent.
| Field | Type | Required | Notes |
|---|---|---|---|
agent_id | string | Yes | One of your voice agents |
curl -X POST https://epicall.ai/console/api/v1/call-tokens \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id":"support_agent"}'{ "agent_id": "support_agent",
"websocket_url": "wss://…/voice-proxy/ws/session?agent_config=support_agent&ticket=…",
"expires_at": "2026-09-30T12:01:00.000Z" }- Connect to
websocket_urland first send{ "client_name": "…", "caller_ref": "your-ref" }. - Stream the caller's audio as binary 16 kHz, 16-bit, mono PCM.
- The agent's audio comes back as binary 24 kHz, 16-bit, mono PCM, with JSON events such as
session_started,user_transcriptandassistant_text. - Close with code 1000. Your
caller_refappears on the call inGET /calls/{id}.
Errors: 400 missing_agent_id, 404 agent_not_found, 402 quota_exceeded, 503 calls_unavailable.
Chat API
Use your chat agents from your own app or server, including handing a conversation to your team.
/chat/messagesServer or website keySends one visitor message and returns the agent's reply.
| Field | Type | Required | Notes |
|---|---|---|---|
agent_id | string | Yes | One of your chat agents |
session_id | string | Yes | 16–128 letters, digits, - or _. Generate one (a UUID) per conversation and reuse it |
text | string | Yes | Up to 4,000 characters |
page_url | string | No | The page the visitor is on |
visitor | object | No | { id, name, email, phone, attributes } — attributes: up to 20 keys |
curl -X POST https://epicall.ai/console/api/v1/chat/messages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id":"support_chat","session_id":"3f6c2b1e-8a9d-4c1b-9f0e-1234567890ab",
"text":"Where is my order A1001?",
"visitor":{"id":"cust-9","name":"Priya","email":"priya@example.com"}}'{ "replies": ["Let me check that for you…"], "handover": { "status": "bot", "assignee": null } }While your team has the conversation, replies is empty; their messages arrive through /chat/updates. Errors: 400 invalid_session_id, 400 missing_text, 413 message_too_long, 404 agent_not_found, 402 quota_exceeded.
/chat/historyServer or website keyThe whole visible conversation. Query: agent_id, session_id.
{ "messages": [ { "role": "user", "text": "Hi", "at": "…" },
{ "role": "assistant", "text": "Hello!", "at": "…" },
{ "role": "team", "text": "Priya here", "at": "…", "author": "Priya" } ],
"handover": { "status": "human", "assignee": "Priya", "team_online": true }, "last_id": 12 }/chat/updatesServer or website keyNew team messages since after (a message id), for polling. Query: agent_id, session_id, after.
/chat/handoverServer or website keyHands the conversation to your team's inbox: { agent_id, session_id, visitor? }. If nobody is online, send { agent_id, session_id, email } so the team can reply by email.
/chat/configServer or website keyThe chat widget's appearance and settings for agent_id, as set in the console.
Website widgets
Paste one tag before </body>. Use a website key that lists your site's domain. Each agent's Integrations tab in the console has these with your agent and key filled in.
<script src="https://epicall.ai/console/widget.js"
data-agent-id="support_agent"
data-key="ek_pk_…"
data-label="Talk to us"
data-position="bottom-right"></script><script src="https://epicall.ai/console/chat-widget.js"
data-agent-id="support_chat"
data-key="ek_pk_…"></script>The chat widget's colours, greeting, pre-chat form, WhatsApp button and “call us” button are set in the console. Pass signed-in visitor details with data-user-* attributes or ChatWidget.identify().
App integrations
Connect these apps in Agent management → Integrations. Your agents can then use them during calls and chats. No code is needed.
| Category | Apps | What agents can do |
|---|---|---|
| Calendar and booking | Google Calendar, Outlook / Microsoft 365, Calendly, Cal.com, Square Appointments, Acuity, Housecall Pro | Check free slots, book, reschedule and cancel |
| CRM | HubSpot, Salesforce, Zoho CRM, Pipedrive, Microsoft Dynamics 365, GoHighLevel | Recognise the caller, save the contact, log the call, create follow-up tasks |
| Helpdesk | Zendesk, Freshdesk, Intercom, Jira Service Management, Salesforce Service Cloud, ServiceNow | Find open tickets, create a ticket with the call summary, add notes, escalate |
| Knowledge | Zendesk Guide, Confluence, Notion | Answer from your help articles |
| Gmail, SendGrid, Postmark, Zoho Mail, Amazon SES | Email a confirmation or follow-up | |
| Payments | Stripe, Square, PayPal | Look up payments, send a payment link, confirm payment |
| Orders and ERP | Shopify, WooCommerce, Oracle NetSuite, Odoo, Dynamics 365 Business Central | Order status and tracking, stock and prices, cancellations, returns |
| Customer engagement | MoEngage, WebEngage, CleverTap | Update the customer profile and record call events |
| Channels | WhatsApp Business, phone calls, website voice and chat | Answer WhatsApp messages and calls, run phone and WhatsApp campaigns |
For anything else, add a custom HTTP tool on the console's Tools screen.
Statuses and outcomes
| Values | |
|---|---|
| Campaign status | draft · pending_approval · scheduled · running · paused · completed · stopped |
| Contact status | pending · calling · retry · callback · done · failed · skipped |
| Call outcome | interested · not_interested · callback · wrong_number · do_not_call · voicemail · no_answer · failed · other · sent |
- Phone campaigns call Indian mobile numbers (+91, starting 6–9). WhatsApp campaigns accept international numbers.
- When someone asks to be called later, the agent books a
callbackat the time they gave. - Outcome
do_not_calladds the number to your do-not-call list for every campaign.
Questions about the EpiCall AI API? Create a key in Deploy Keys and try the examples above; every one runs as written. No account yet? Start a free trial or talk to us.
Ready to put an AI voice agent on every call?
Talk to us about your call volumes, languages, and channels — we'll show you EpiCall AI handling a real conversation in under a week.
Talk to Sales