# Kesy API > Kesy is an AI phone agent. This API lets a business pull its call reports and campaign > data, and receive signed webhooks when calls happen. It is account-scoped: an API key > only ever accesses its own business's data. Available on any paid Kesy plan. - Base URL: `https://api.kesy.ai/api/v1` - Auth: every request needs an API key created in the Kesy dashboard (Settings → Developers & API). Send it as a bearer token: `Authorization: Bearer kesy_live_...` - Content type: JSON. Times are ISO 8601 (UTC). - Rate limit: 120 requests/minute per API key. - OpenAPI spec: `https://api.kesy.ai/api/v1/openapi.json` - Human docs: https://docs.kesy.ai ## Authentication ``` curl https://api.kesy.ai/api/v1/calls -H "Authorization: Bearer kesy_live_YOUR_KEY" ``` Errors are JSON: `{ "error": "...", "code": "..." }`. Common codes: - 401 `invalid_api_key` — missing/invalid/revoked key - 403 `api_access_requires_paid_plan` — the account has no active paid plan - 429 `rate_limited` — too many requests ## Endpoints (read API) ### GET /calls List call reports (inbound + outbound), newest first. Query params: `limit` (default 25, max 100), `offset`, `direction` (inbound|outbound), `campaign_id`, `status`, `from` (ISO), `to` (ISO). Response: `{ "object": "list", "data": [Call], "limit", "offset", "has_more" }` ### GET /calls/{id} Retrieve one call by id. Returns a Call object (404 if not found). ### GET /campaigns List campaigns. Query: `limit`, `offset`, `status`. Returns `{ object:"list", data:[Campaign] }`. ### GET /campaigns/{id} Retrieve one campaign by id. ### POST /campaigns/{id}/calls (trigger a call) Place an outbound call on an API-type campaign. The campaign’s script, voice, and persona are configured once in the dashboard; each request just pushes one lead. The call result arrives on the `call.completed` webhook — correlate it via the `variables` you send. Body: `{ phone (required), name?, variables? (object), idempotency_key? }` Response `202`: `{ "object": "call_request", "id": "...", "status": "queued", "campaign_id": "...", "variables": {...} }` ```bash curl -X POST https://api.kesy.ai/api/v1/campaigns/CAMPAIGN_ID/calls \ -H "Authorization: Bearer kesy_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "+919812345678", "name": "Ravi Kumar", "variables": { "order_id": "4821", "amount": "1299", "product": "Wireless earbuds" }, "idempotency_key": "order-4821" }' ``` `variables` are merged into the AI’s script for that call (e.g. it can say “confirming your COD order #4821 for ₹1,299”) and are echoed back on the webhook. Send an `idempotency_key` to make retries safe — a repeated key is accepted without dialing again. ## The Call object ```json { "id": "a1b2c3d4-...", "object": "call", "direction": "outbound", "status": "completed", "business_id": "b1...", "campaign_id": "c1...", "contact": { "name": "Ravi Kumar", "phone": "+919812345678", "company": null }, "started_at": "2026-07-23T09:15:02Z", "ended_at": "2026-07-23T09:16:20Z", "duration_seconds": 78, "outcome": "Interested", "sentiment": "positive", "summary": "Confirmed COD order #4821 for the wireless earbuds.", "transcript": [ { "speaker": "ai", "text": "Hello, am I speaking with Ravi?" }, { "speaker": "customer", "text": "Yes, speaking." } ], "recording_url": "https://.../recording.mp3", "attempt_number": 1, "variables": { "order_id": "4821", "amount": "1299" }, "provider_call_sid": "exotel-sid-..." } ``` Fields: `id`, `direction` (inbound|outbound), `status`, `campaign_id` (nullable), `contact` {name, phone, company}, `started_at`, `ended_at`, `duration_seconds`, `outcome` (AI-classified: Interested, Not Interested, Callback, Converted, No Answer, ...), `sentiment` (positive|neutral|negative), `summary`, `transcript` [{speaker: ai|customer|system, text}], `recording_url` (may be null until the `call.recording_ready` webhook), `attempt_number`, `provider_call_sid`. ## The Campaign object ```json { "id": "c1...", "object": "campaign", "name": "July COD confirmations", "type": "simple", "status": "active", "total_contacts": 120, "calls_made": 44, "calls_answered": 31, "created_at": "2026-07-20T10:00:00Z" } ``` ## Webhooks Register HTTPS webhook URLs in the dashboard (Settings → Developers & API) and choose events. Kesy POSTs a JSON body `{ "event": "...", "created_at": "...", "data": {...} }` when each event fires. `data` is a Call object for `call.*` events and a Campaign object for `campaign.*` events. ### Events - `call.started` — An outbound call was answered / connected. - `call.completed` — A call ended. Includes the full report: outcome, sentiment, transcript, summary, duration. - `call.recording_ready` — The call recording URL is now available (arrives ~1–3 min after the call). - `call.transferred` — A live call was bridged to a human team member. - `campaign.started` — A campaign was launched. - `campaign.completed` — A campaign finished all of its calls. ### Verifying the signature Each POST includes headers `X-Kesy-Event`, `X-Kesy-Delivery`, `X-Kesy-Timestamp`, and `X-Kesy-Signature: sha256=`. Compute HMAC-SHA256 of the raw request body using your endpoint's signing secret (shown once when you create the webhook) and compare (constant-time). Node.js: ```js const crypto = require('crypto'); function verify(rawBody, header, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header || '')); } ``` Python: ```python import hmac, hashlib def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header or '') ``` Return any 2xx to acknowledge. Failed deliveries are retried with exponential backoff.