Get API Key

Audatec Voice AI

An AI Call Platform that gives your application a real voice. Trigger AI-powered outbound calls at scale — outcomes delivered instantly.

AI Agents
Configure conversational AI agents with custom scripts and per-call context injection.
Instant Dispatch
Calls are dispatched within seconds. SIP trunks handle hundreds of concurrent channels.
Real-time Results
Receive each call's result — status, outcome, and the agent's disposition — the moment the call ends, by webhook or polling.

How it works

Every call flows through three components:

  • Agents — an AI persona with a script, voice, and expected input fields (e.g. customer_name, amount_due)
  • SIP Trunks — carrier lines with concurrency limits assigned to your org
  • Calls — a request that combines a phone number, an agent, a trunk, and per-call metadata

Base URL

Base URL
https://api.audatec.in/integration/v1
ℹ️ All requests must be made over HTTPS. HTTP requests will be rejected.

Authentication

Every request to the API must include an API key in the Authorization header.

API Key Format

Key structure
audtk_Wg8xJCrw-I6n3h48v6ZfTFgUzpADscHid36SCM

Using your key

Include your key in the Authorization header on every request:

HTTP Header
Authorization: Bearer audtk_your_key_here

Getting a key

Generate API keys from the Audatec Dashboard:

  1. Log in to studio.audatec.in
  2. Navigate to Settings → Integration
  3. Click New API Key, give it a name, and select a role
  4. Copy the key immediately — it is shown only once
⚠️ The raw key is shown exactly once at creation and cannot be retrieved again. Store it in a secrets manager immediately.

Key roles

Each key carries a role that controls what it can do:

viewer List agents & trunks, check call status
member Everything in viewer + trigger calls
admin Everything in member + manage webhooks
owner Unrestricted org-level access

A request outside the key's role is refused with 403 and "code": "insufficient_scope".

Keys are org-scoped — a key authenticates as the organisation, not an individual user. All agents and trunks belonging to your org are accessible with a valid key. You can also send the key as an X-Client-Key header instead of Authorization.

Quick Start

Place your first AI call in three steps.

1
Get your agent ID
List available agents and note the id and required input_schema.fields.
curl
curl https://api.audatec.in/integration/v1/agents \
  -H "Authorization: Bearer $API_KEY"
2
Get your SIP trunk ID
List available trunks and note the id of an active one.
curl
curl https://api.audatec.in/integration/v1/sip-trunks \
  -H "Authorization: Bearer $API_KEY"
3
Trigger a call
Pass the phone number, agent, trunk, and any metadata the agent needs.
curl
curl -X POST https://api.audatec.in/integration/v1/calls \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+919876543210",
    "agent_id": "830f767a-397e-4b39-82ff-235cd344e2f9",
    "sip_trunk_id": "d5f3a1c0-8b2e-4f9a-b7d1-1e2c3a4b5c6d",
    "metadata": {
      "customer_name": "Rahul Sharma",
      "amount_due": "7500",
      "due_date": "2026-06-20"
    }
  }'
✅ A successful response returns a call_id. Use it to poll /calls/{call_id}/status, or subscribe a webhook to receive the result automatically.

List Agents

Returns the active AI agents available to your organisation, including the metadata fields each agent requires.

GET https://api.audatec.in/integration/v1/agents

Response — 200 OK

JSON
{
  "agents": [
    {
      "id": "830f767a-397e-4b39-82ff-235cd344e2f9",
      "name": "EMI Reminder Agent",
      "status": "active",
      "has_live_version": true,
      "input_schema": {
        "fields": ["customer_name", "amount_due", "due_date"],
        "sample_context": {
          "customer_name": "Rahul Sharma",
          "amount_due": "7500",
          "due_date": "2026-06-20"
        }
      }
    }
  ]
}

Response fields

FieldTypeDescription
idstringUnique agent UUID. Pass this as agent_id when triggering a call.
namestringHuman-readable display name.
statusstringOnly active agents are listed, and only active agents can be used to trigger calls.
has_live_versionbooleantrue when the agent has a published version. An agent without one cannot be used to trigger a call.
input_schema.fieldsstring[]Keys that must be present in metadata when triggering a call with this agent.
input_schema.sample_contextobjectExample values showing the expected format for the fields that have one.

List SIP Trunks

Returns the active SIP trunks (carrier lines) your organisation has been allocated, with your concurrency and rate limits on each.

GET https://api.audatec.in/integration/v1/sip-trunks

Response — 200 OK

JSON
{
  "sip_trunks": [
    {
      "id": "d5f3a1c0-8b2e-4f9a-b7d1-1e2c3a4b5c6d",
      "provider_trunk_id": "ST_xxxxxxxxxxxx",
      "trunk_phone_number": "+918069XXXXXX",
      "max_cps": 5,
      "max_channels": 100,
      "status": "active"
    }
  ]
}

Response fields

FieldTypeDescription
idstringUnique trunk UUID. Pass this as sip_trunk_id when triggering a call.
provider_trunk_idstringThe carrier-side trunk identifier. For reference only.
trunk_phone_numberstring | nullThe caller ID shown to the recipient.
max_cpsnumberCalls per second allocated to your organisation on this trunk. Exceed it and you receive 429.
max_channelsnumberSimultaneous active calls allocated to your organisation on this trunk. Exceed it and you receive 429.
statusstringOnly active trunks are listed and usable.

Trigger a Call

Dispatch a single outbound AI call. The call is accepted immediately and your agent starts the conversation when the recipient answers. Calls are never queued: if your trunk allocation is full you get a 429 and should retry.

Requires a key with the member role or higher. A viewer key (or a results-only key) gets 403.

POST https://api.audatec.in/integration/v1/calls

Request headers

HeaderRequiredDescription
AuthorizationrequiredBearer audtk_...
Content-Typerequiredapplication/json
Idempotency-KeyoptionalA unique string for this call, e.g. loan-84213-reminder-2026-06-20. If you retry a request with the same key, Audatec returns the original call instead of dialling again. See Retrying safely.

Request body

ParameterTypeDescription
phone_numberrequired string The number to call, 10–15 characters. Use E.164 format — +919876543210 for India, +14155552671 for the US. A number outside that length is rejected with 502.
agent_idrequired string (UUID) ID of the agent that will handle the call. Obtain from List Agents. The agent must be active and have a published version.
sip_trunk_idrequired string (UUID) ID of the trunk to place the call through. Obtain from List SIP Trunks.
metadataconditional object Per-call variables injected into the agent's conversation (e.g. customer name, loan amount). Must contain every entry in the agent's input_schema.fields; a missing, null or empty-string value counts as missing. Required if the agent has any fields.
callback_urloptional string (URL) An http(s) endpoint that receives a one-time, best-effort notification for this call. For reliable delivery use a webhook subscription instead. See Call Notifications.

Example request

JSON body
{
  "phone_number": "+919876543210",
  "agent_id": "830f767a-397e-4b39-82ff-235cd344e2f9",
  "sip_trunk_id": "d5f3a1c0-8b2e-4f9a-b7d1-1e2c3a4b5c6d",
  "metadata": {
    "customer_name": "Rahul Sharma",
    "amount_due": "7500",
    "due_date": "2026-06-20"
  }
}

Responses

201 Created Call accepted
JSON
{
  "call_id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "status": "triggered",
  "poll_url": "/integration/v1/calls/31106b7c-9d02-4723-8b07-5bb8d90240cb/status"
}
200 OK Duplicate — the Idempotency-Key was already used; no new call was placed
JSON
{
  "call_id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "status": "duplicate",
  "poll_url": "/integration/v1/calls/31106b7c-9d02-4723-8b07-5bb8d90240cb/status"
}
429 Capacity or rate limit reached — check Retry-After header
JSON
{
  "error": "Trunk concurrency (max_channels) exhausted",
  "call_id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "status": "rejected"
}
💡 On a 429 the call is not queued. Read the Retry-After header for the seconds to wait, then send the request again. A rejected attempt still gets a call_id and ends as network_failed.
502 The dialer is unavailable or rejected the request — retry with backoff
JSON
{
  "error": "Dialer unavailable",
  "call_id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "status": "failed"
}

400 / 404 return { "error": "..." } — see Error Handling for the messages.

Retrying safely

If a request times out you cannot tell whether the call was placed. Send an Idempotency-Key and retry with the same key: you get the original call_id back (200, "status": "duplicate") and the customer is not called twice.

  • Use one key per intended call — for example built from your own record, like loan-84213-reminder-2026-06-20 — and reuse it on every retry of that call.
  • A new call to the same customer (tomorrow's reminder) needs a new key.
  • Keys are scoped to your API key.
  • Send retries one after another, not simultaneously.

Get Call Status

Returns the current state and outcome of a call. Poll this endpoint until is_terminal is true.

GET https://api.audatec.in/integration/v1/calls/{call_id}/status

Path parameters

ParameterDescription
call_idrequiredThe UUID returned in the call_id field when you triggered the call.

Response — 200 OK

JSON
{
  "id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "phone_number": "+919876543210",
  "status": "ended",
  "outcome": "connected",
  "is_terminal": true,
  "disposition": "PROMISE_TO_PAY",
  "duration_secs": 97,
  "started_at": "2026-06-14T10:32:05.000Z",
  "ended_at": "2026-06-14T10:33:42.000Z",
  "transcript_url": "https://[bucket-id].s3.ap-south-1.amazonaws.com/[resource-id]",
  "recording_url": "https://[bucket-id].s3.ap-south-1.amazonaws.com/[resource-id]"
}

Response fields

FieldTypeDescription
statusstringWhere the call is: queued, ringing, in_progress or ended. It only ever moves forward. See Call Status Values.
outcomestring | nullHow the call ended (connected, busy, no_answer, …). null until the call has ended.
is_terminalbooleanStop polling when this is true. The call has ended.
dispositionstring | nullThe agent's own classification of the conversation (for example PROMISE_TO_PAY). The set of values depends on the agent. null if the call did not connect or no result has been recorded yet.
duration_secsnumber | nullConversation duration in seconds. null for unanswered calls.
started_atstring | nullISO 8601 timestamp of when the recipient answered.
ended_atstring | nullISO 8601 timestamp of when the call ended.
transcript_urlstring | nullLink to the call transcript, if enabled for your account.
recording_urlstring | nullLink to the call recording, if enabled for your account.

Returns 404 if the call does not exist or belongs to another organisation. Requires a key that can read calls (viewer role or higher, with the calls scope).

Results Access

How access to campaign results works: what a key can read, how to get one, and the limits that apply.

The results key

Campaign results are read with a results key, an API key that holds the results:read scope. It is sent like any other key, as Authorization: Bearer audtk_…, and works only on the results endpoints: Campaigns & Runs, Get Results and Get Summary. On any other endpoint, such as POST /calls, it returns 403 with "code": "insufficient_scope". The scope is held by the viewer role and up.

Which campaigns a key can see

Anyone in your organisation who can create API keys can create a results key and read all of the organisation's campaign results with it, so keep keys as private as the data itself.

  • A results key belongs to your organisation. It reads the results of every campaign in that organisation, and never of any other organisation.
  • GET /campaigns lists your organisation's campaigns (paged).
  • A campaign that does not exist, was deleted, or belongs to another organisation returns the same 404 ("Campaign not found"), so a key cannot discover other organisations' campaigns.
  • A run that is not part of the campaign you ask about returns 404 ("Run not found").

Which fields a key can see

The fields are fixed by Audatec for each campaign, according to the campaign's agent. Each campaign's result_schema (in Get Campaign) lists their names, types and meanings. Nothing else is ever returned: no raw call data, no costs, and no columns from your uploaded contact file other than the input variables your agent is declared to take (returned as metadata, below). (The transcript and recording links are included, for connected calls only.) A campaign whose agent has no dedicated field set returns the common fields only. If you need another field, ask Audatec to add it.

Getting a key

Create a key under Settings → Integration in the Audatec Voice Platform (organisation admins), or ask Audatec to issue one with the results scope. The key is shown once, so store it securely. Regenerate replaces a key with a new secret that keeps the same name, role and scopes, and revokes the old one at once. If a key is lost or exposed, revoke it.

Limits

To protect the service and your data, each results key has limits: 250 rows per page, 60 requests a minute, and 500,000 result rows a day (resets at 00:00 UTC). Results are always read one run at a time. See Rate Limits & Paging for the details, the error responses and how to read results politely.

Campaigns & Runs

Read the campaigns you have been given access to, the fields their results contain, and their runs. Read-only.

🔑 Results access needs a results key for your organisation: it reads all of the organisation's campaigns, and each campaign returns only the fields defined for it. A campaign of another organisation returns 404. See Results Access.
GET https://api.audatec.in/integration/v1/campaigns

Your organisation's campaigns, newest first. Paged: limit (1–200, default 100) and cursor (the previous response's pagination.next_cursor).

JSON
{
  "campaigns": [
    {
      "id": "0b7c9a52-6f0e-4c3d-9a7e-2f6d1c8e4b10",
      "name": "Gold Loan — Pre-Auction Reminder",
      "status": "active",
      "created_at": "2026-08-01T05:30:00.000Z",
      "last_run_at": "2026-09-29T09:10:00.000Z"
    }
  ],
  "pagination": { "limit": 100, "has_more": false, "next_cursor": null }
}
GET https://api.audatec.in/integration/v1/campaigns/{campaign_id}

One campaign, plus result_schema — the complete list of fields (name, type, description) that its result rows contain, and metadata_fields, the names of the input variables returned in each row's metadata. This is the contract for the results of that campaign; it works like input_schema on List Agents. result_schema.version changes only when fields change.

JSON
{
  "id": "0b7c9a52-6f0e-4c3d-9a7e-2f6d1c8e4b10",
  "name": "Gold Loan — Pre-Auction Reminder",
  "status": "active",
  "created_at": "2026-08-01T05:30:00.000Z",
  "last_run_at": "2026-09-29T09:10:00.000Z",
  "result_schema": {
    "version": "iifl_gl_v1",
    "description": "IIFL Gold Loan pre-auction reminder",
    "fields": [
      { "name": "phone_number", "type": "string", "description": "The contact's phone number." },
      { "name": "disposition", "type": "string|null", "description": "The agent's classification of the conversation." },
      { "name": "metadata", "type": "object", "description": "The call's input variables (what the agent was given), by variable name; see metadata_fields." },
      { "name": "promise_to_pay", "type": "boolean|null", "description": "The customer promised to pay." }
    ],
    "metadata_fields": ["callee_name", "dpd", "total_os_bucket", "min_amount_to_stop_auction", "branch_name"]
  }
}
GET https://api.audatec.in/integration/v1/campaigns/{campaign_id}/runs

The campaign's runs, newest first — every run, whatever its status (completed, cancelled, failed, …). A run is one attempt of a batch. Results are always read for one run, so use a run's id as the run_id of Get Results and Get Summary. Paged: limit (1–200, default 100) and cursor (the previous response's pagination.next_cursor). To see only the runs of a time period, add from and/or to (ISO 8601; from inclusive, to exclusive; a bare date such as 2026-09-05 means 00:00 UTC). A run belongs to a period by the time it was created. The response repeats the period you asked for as "period": { "from": …, "to": … } (null for an open end).

JSON
{
  "period": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-10-01T00:00:00.000Z" },
  "runs": [
    {
      "id": "9f1d2e3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
      "attempt_no": 2,
      "previous_run_id": "3c7e1b90-5d2a-4f18-a6b4-91e0d8c2f4a7",
      "status": "completed",
      "total_rows": 1583,
      "processed_rows": 1583,
      "success_rows": 940,
      "failed_rows": 643,
      "scheduled_at": null,
      "started_at": "2026-09-29T09:10:00.000Z",
      "completed_at": "2026-09-29T11:42:00.000Z"
    }
  ],
  "pagination": { "limit": 100, "has_more": false, "next_cursor": null }
}
FieldTypeDescription
idstringRun UUID.
attempt_nonumber1 for the first pass of a batch; 2, 3, … for the automatic retry passes that follow it.
previous_run_idstring | nullFor a smart-retry pass (attempt 2 or later), the id of the run it retries — attempt 2 points at attempt 1, attempt 3 at attempt 2. null for the first pass and for every run that is not a smart-retry pass; those stand alone and are not linked to any other run.
statusstringWhere the run is (for example running, completed).
total_rows · processed_rows · success_rows · failed_rowsnumber | nullProgress counters for the run.
scheduled_at · started_at · completed_atstring | nullISO 8601 timestamps.

Get Results

One row per contact in one run, showing the contact's final outcome in that run. Paged and filterable, so a dashboard can stay in sync cheaply.

GET https://api.audatec.in/integration/v1/campaigns/{campaign_id}/results

Query parameters

ParameterTypeDescription
run_idrequiredstring (UUID)The run to read: an id from Campaigns & Runs. A run that does not belong to this campaign returns 404. Without it the request is refused with 400.
limitintegerRows per page, 1–250. Default 100. To read more, follow pagination.next_cursor.
cursorstringPass the previous response's pagination.next_cursor to get the next page.
from · tostring (ISO 8601)Only calls placed in this period (from inclusive, to exclusive; a bare date means 00:00 UTC). Either may be omitted. The contact's final outcome, attempts_made and first/last attempted times are then worked out from those calls only. to must be later than from. The response repeats the period as period.
outcomestringOnly contacts whose final outcome is this value: connected, voicemail, busy, no_answer, declined, network_failed, internal_error, rtc_error, connecthub_error.
dispositionstringOnly contacts with this exact disposition.
updated_sincestring (ISO 8601)Only contacts whose updated_at is at or after this time. Use it to fetch just what changed.

Response — 200 OK

JSON
{
  "run": { "id": "9f1d2e3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f", "attempt_no": 2, "previous_run_id": "3c7e1b90-5d2a-4f18-a6b4-91e0d8c2f4a7" },
  "period": { "from": null, "to": null },
  "results": [
    {
      "phone_number": "+919876543210",
      "run_id": "9f1d2e3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
      "status": "ended",
      "outcome": "connected",
      "is_terminal": true,
      "disposition": "PROMISE_TO_PAY",
      "duration_secs": 97,
      "transcript_url": "https://[bucket-id].s3.ap-south-1.amazonaws.com/[transcript-id]",
      "recording_url": "https://[bucket-id].s3.ap-south-1.amazonaws.com/[recording-id]",
      "attempts_made": 2,
      "first_attempted_at": "2026-09-28T09:11:00.000Z",
      "last_attempted_at": "2026-09-29T09:14:00.000Z",
      "updated_at": "2026-09-29T09:15:40.000Z",
      "metadata": {
        "callee_name": "Rahul Kumar",
        "dpd": "92",
        "total_os_bucket": "1750155.76",
        "min_amount_to_stop_auction": "21955.7",
        "branch_name": "Connaught Place"
      },
      "promise_to_pay": true,
      "payment_timeline": "Friday"
    }
  ],
  "pagination": { "limit": 100, "has_more": true, "next_cursor": "eyJwIjoiKzkxOTg3NjU0MzIxMCJ9" }
}

run says which run the rows belong to (previous_run_id is null unless it is a smart-retry pass). Rows are ordered by phone_number. The fields in each row are exactly those listed in the campaign's result_schema; the example above shows the common fields plus a few campaign-specific ones. A field with no value is null.

Common fields

FieldTypeDescription
phone_numberstringThe contact's phone number. Unique in the results.
run_idstringThe run these results belong to (the same in every row).
status · outcome · is_terminalstring · string | null · booleanSee Call Status Values. Describes the final attempt.
dispositionstring | nullThe agent's classification of the conversation. The values depend on the agent.
duration_secsnumber | nullConversation duration of the final attempt.
transcript_url · recording_urlstring | nullLinks to the call transcript and recording of the contact's final attempt. Only a connected call has them; they are null for every other outcome, and for a connected call until they have been saved. Treat the links as confidential.
metadataobjectThe call's input variables: the values your contact file gave the agent for this call (for example customer name, days past due, outstanding amount, branch), keyed by variable name. The keys are the same for every row and are listed in the campaign's result_schema.metadata_fields. Every value is a string, or null if that call had none. Input variables are saved only for calls placed after this was introduced; for earlier calls every value is null. This is customer personal data: treat it as confidential.
attempts_madenumberHow many times the contact was dialled.
first_attempted_at · last_attempted_atstringISO 8601 timestamps of the first and the most recent attempt.
updated_atstringWhen anything about this contact last changed. Use as the updated_since checkpoint.

What "final outcome" means

The row shown is the contact's result within this run: the call where they connected, if they did; otherwise their most recent call in the run. attempts_made counts the calls made to them in this run. A contact retried in a later pass (a run with previous_run_id) has a separate row, with its own result, in that later run — read each run you want.

Keeping a dashboard in sync

Don't re-read every row on each refresh. Per run, store the largest updated_at you have seen, and next time ask only for rows changed since then. Each contact appears once per run, so overwrite by phone_number within that run. Runs that have completed rarely change; keep refreshing only the ones still in progress.

Node.js — incremental sync
let since = loadCheckpoint(RUN_ID);      // ISO timestamp, or null the first time for this run
let cursor = null, newest = since;

do {
  const url = new URL(`${BASE}/campaigns/${CAMPAIGN_ID}/results`);
  url.searchParams.set('run_id', RUN_ID);
  url.searchParams.set('limit', '500');
  if (since) url.searchParams.set('updated_since', since);
  if (cursor) url.searchParams.set('cursor', cursor);

  const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
  const { results, pagination } = await res.json();

  for (const row of results) {
    upsertByPhone(row);                  // one row per phone_number: overwrite what you have
    if (!newest || row.updated_at > newest) newest = row.updated_at;
  }
  cursor = pagination.has_more ? pagination.next_cursor : null;
} while (cursor);

saveCheckpoint(RUN_ID, newest);          // next time: updated_since = newest
💡 Refresh no more often than about once a minute (the limit is 60 requests a minute per key). Results are calculated on request, so large campaigns are best read incrementally with updated_since.

Usage limits

Results are meant to be read in small pages and, after the first read, incrementally. Each results key is limited to:

  • 60 requests a minute across all the results endpoints (X-RateLimit-Limit and X-RateLimit-Remaining are returned on every response);
  • 500,000 result rows a day on /results (resets at 00:00 UTC; X-Daily-Rows-Remaining shows what is left);
  • 250 rows per page, and a query that runs longer than about 20 seconds is stopped.

Over a limit you receive 429 with a Retry-After header (seconds) and a code of rate_limited or daily_quota_exceeded. Use updated_since to fetch only what changed, and read a finished run once. See Rate Limits & Paging for how the limits count, how to retry, and how to page through results. Contact us if you need higher limits.

Errors

StatusWhen
400run_id missing or not a UUID; from/to not a date or timestamp, or to not later than from; or limit outside 1–250, malformed cursor, unknown outcome, updated_since not a timestamp.
403The key is not a results key (insufficient_scope).
404The campaign does not exist, was deleted or belongs to another organisation, or the run is not part of the campaign.
429rate_limited (more than 60 requests in a minute), daily_quota_exceeded (today's row allowance used up) or results_busy (too many queries running at once). Wait the Retry-After seconds.
503results_timeout: the query took too long. Retry after the Retry-After seconds, in smaller pages or with updated_since.

Get Summary

Counts for one run, computed for you — use this for totals and charts instead of paging through every result.

GET https://api.audatec.in/integration/v1/campaigns/{campaign_id}/summary

Required query parameter run_id (from Campaigns & Runs). Without it the request is refused with 400; a run that is not part of the campaign returns 404. Optional from / to (ISO 8601, from inclusive, to exclusive) count only calls placed in that period, exactly as in Get Results; the response repeats it as period.

Response — 200 OK

JSON
{
  "campaign_id": "0b7c9a52-6f0e-4c3d-9a7e-2f6d1c8e4b10",
  "run_id": "9f1d2e3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "attempt_no": 2,
  "previous_run_id": "3c7e1b90-5d2a-4f18-a6b4-91e0d8c2f4a7",
  "period": { "from": null, "to": null },
  "as_of": "2026-09-30T10:00:00.000Z",
  "planned": 9800,
  "contacts": 9210,
  "connected": 4900,
  "by_outcome": { "connected": 4900, "no_answer": 3100, "busy": 800, "in_progress": 410 },
  "by_disposition": { "PROMISE_TO_PAY": 1500, "CALLBACK_REQUESTED": 320, "none": 4310 },
  "promise_to_pay": 1500
}
FieldDescription
attempt_no · previous_run_idWhich pass this run is, and (for a smart-retry pass) the run it retries; null otherwise.
plannedContacts scheduled for this run. null if the run has no recorded row count.
contactsContacts dialled at least once in this run. Each is counted once, by final outcome.
connectedContacts whose final outcome is connected.
by_outcomeContacts per final outcome. in_progress means an attempt has not ended yet.
by_dispositionContacts per disposition. none means no disposition was recorded.
promise_to_payPresent only for campaigns whose result_schema supports it: the number of contacts who promised to pay.
as_ofWhen the counts were calculated.

Manage Webhooks

Subscribe an endpoint to call events. This is the recommended way to receive results: deliveries are signed, retried, and cover every call your organisation places through this API. Requires an admin (or owner) key.

MethodPathDescription
POST/integration/v1/webhooksCreate a subscription. Returns the signing secret once.
GET/integration/v1/webhooksList your subscriptions: { "webhooks": [ … ] }.
PATCH/integration/v1/webhooks/{id}Change url, events or is_active.
DELETE/integration/v1/webhooks/{id}Delete a subscription. Returns 204.

List — response 200

JSON
{
  "webhooks": [
    {
      "id": "5b0c2f5e-3c1d-4e0a-9d64-0b9d7a1f2c88",
      "url": "https://your-server.com/hooks/audatec",
      "events": ["call.completed", "call.failed", "call.rejected"],
      "is_active": true
    }
  ]
}

The signing secret is never returned by the list or update calls — only once, when the subscription is created.

Create — request

JSON body
{
  "url": "https://your-server.com/hooks/audatec",
  "events": ["call.completed", "call.failed", "call.rejected"]
}
ParameterTypeDescription
urlrequiredstring (URL)An http(s) endpoint. Use HTTPS in production.
eventsrequiredstring[]One or more of call.answered, call.completed, call.failed, call.rejected. See Call Notifications.

Create — response 201

JSON
{
  "id": "5b0c2f5e-3c1d-4e0a-9d64-0b9d7a1f2c88",
  "url": "https://your-server.com/hooks/audatec",
  "events": ["call.completed", "call.failed", "call.rejected"],
  "is_active": true,
  "secret": "whsec_kT3...",
  "warning": "Store this secret now — it is needed to verify delivery signatures and is not retrievable later."
}
⚠️ The secret is shown only once. Store it — you need it to verify the X-Audatec-Signature header on every delivery.

Update — request

PATCH /integration/v1/webhooks/{id} with any of the fields below; the ones you leave out stay as they are. Send at least one.

JSON body
{
  "events": ["call.completed", "call.failed"],
  "is_active": false
}
ParameterTypeDescription
urlstring (URL)New http(s) endpoint.
eventsstring[]Replaces the subscribed events. Same allowed values as on create.
is_activebooleanfalse pauses deliveries without deleting the subscription.

Returns 200 with the updated subscription (id, url, events, is_active; no secret).

Errors

Error messageStatusCause / fix
url must be a valid http(s) URL400Missing or malformed url on create, or an invalid one on update.
events must be a non-empty subset of: call.answered, call.completed, call.failed, call.rejected400events is missing, empty, or contains a name that is not listed.
Nothing to update400A PATCH with none of url, events or is_active.
Webhook not found404The id does not exist or belongs to another organisation (update and delete).
This API key does not have the required scope403The key is below admin.

Call Notifications

Receive call results without polling. There are two ways: a webhook subscription (recommended — durable, retried) or a per-call callback_url (one best-effort attempt).

ℹ️ Only calls created through this API produce notifications. Each event is sent at most once per call.

Notification payload

JSON
{
  "event": "call.completed",
  "call_id": "31106b7c-9d02-4723-8b07-5bb8d90240cb",
  "status": "ended",
  "outcome": "connected",
  "duration_secs": 97,
  "disposition": "PROMISE_TO_PAY",
  "transcript_url": null,
  "recording_url": null,
  "occurred_at": "2026-06-14T10:33:42.000Z"
}
JSON
{
  "event": "call.failed",
  "call_id": "91a2b3c4-d5e6-7f8a-9b0c-1d2e3f4a5b6c",
  "status": "ended",
  "outcome": "no_answer",
  "duration_secs": null,
  "disposition": null,
  "transcript_url": null,
  "recording_url": null,
  "occurred_at": "2026-06-14T11:05:10.000Z"
}

The payload carries the call's status and outcome (see Call Status Values). disposition, duration_secs and the URLs are only filled in when the agent recorded a result (normally on call.completed); on the other events they are null. Your own metadata is not included — match on call_id.

Event types

EventSent when
call.answeredThe recipient picked up and the conversation is starting.
call.completedThe call ended after a connected conversation (outcome: connected).
call.failedThe call ended without a connected conversation — busy, no answer, declined, voicemail, or a network/carrier error.
call.rejectedYour trunk allocation had no capacity when you triggered the call (the 429 case).

Delivery headers

HeaderDescription
X-Audatec-EventThe event name, e.g. call.completed.
X-Audatec-DeliveryA unique id for this delivery. Delivery is at-least-once, so deduplicate on this id.
X-Audatec-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret.

Signature verification

Verify the signature on the raw body before processing:

Node.js
const crypto = require('crypto');

// rawBody: the exact request body bytes (do not re-serialise the parsed JSON)
function verifySignature(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(signatureHeader || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Endpoint requirements & retries

  • Return HTTP 2xx within 10 seconds. If processing takes longer, respond 200 immediately and handle the payload asynchronously.
  • Webhook subscriptions are retried on a non-2xx response or timeout: after 10 s, 1 min, 5 min, 30 min and 2 h — six attempts in total, after which the delivery is dropped.

callback_url (per call)

⚠️ callback_url is best-effort: one attempt, no retries, and it is not sent for every event — it is not sent when a call fails at the carrier (busy, no answer) or when a call is answered. Use a webhook subscription when you need every result.
  • Same payload and headers as above.
  • Signed with the secret of your organisation's first active webhook subscription. If you have none, it is signed with a shared default secret, so you cannot verify it — create a subscription to get a secret you own.

Webhooks vs callback_url vs Polling

Three ways to receive call results. Use a webhook subscription for any production system.

Webhook subscription Recommended

Create a subscription once with POST /webhooks. Audatec then POSTs every selected event for every call you place through the API — no repeated requests and no per-call setup.

  • Reliable — failed deliveries are retried for hours, and each delivery has a unique id so you can deduplicate
  • Complete — answered, completed, failed and rejected calls are all covered
  • Scales linearly — 1000 concurrent calls still means about 1000 deliveries, not 1000 × N polls
  • Verified with X-Audatec-Signature (HMAC-SHA256) so you can reject spoofed requests
💡 If your endpoint responds slowly, return 200 immediately and process the payload asynchronously. Audatec retries on non-2xx or timeout, so a fast ack prevents duplicate processing.

callback_url

A URL passed on a single trigger request. Convenient for quick tests, but one best-effort attempt only: no retries, and not sent for carrier-level failures. Do not rely on it for production results.

Polling

If you can't expose a public endpoint (local development, scripts, one-off jobs), poll GET /calls/{call_id}/status until is_terminal is true.

  • No inbound server required
  • Keep the interval at 4–5 seconds minimum — tighter loops waste quota without benefit
  • Stop immediately once is_terminal: true
  • Not recommended at scale: 100 concurrent calls at 4 s intervals = 25 req/s of pure overhead
Node.js — polling loop
async function waitForCall(callId, apiKey) {
  const url = `https://api.audatec.in/integration/v1/calls/${callId}/status`;
  while (true) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` }
    });
    const data = await res.json();
    if (data.is_terminal) return data;
    await new Promise(r => setTimeout(r, 4000)); // poll every 4s
  }
}

Error Handling

All errors return JSON with an error field describing what went wrong.

Error response shape
{ "error": "metadata missing required keys: amount_due, due_date" }

Common errors

Error messageStatusCause / fix
phone_number is required400Missing or empty field. Include phone_number in E.164 format, e.g. +919876543210.
agent_id (uuid) is required / sip_trunk_id (uuid) is required400Missing or not a UUID. Take the ids from GET /agents and GET /sip-trunks.
metadata missing required keys: X400The agent's required fields are not all present. Check input_schema.fields from GET /agents.
metadata must be an object400metadata must be a JSON object.
Agent is not active400The agent has been deactivated. Use GET /agents to find an active agent.
Agent has no live version published400The agent has no published version yet (has_live_version is false).
SIP trunk is not active400The trunk is inactive. Use GET /sip-trunks to find an active trunk.
callback_url must be a valid http or https URL400Malformed URL. Ensure it starts with https:// or http://.
Agent not found / SIP trunk not found404The id does not exist or belongs to another organisation.
No active allocation for this trunk404Your organisation has no active allocation on this trunk. Use a trunk from GET /sip-trunks.
Invalid or inactive API key401Key revoked or wrong. Generate a new key from Settings → Integration.
This API key does not have the required scope403The key's role cannot do this (for example a viewer key triggering a call, or a member key managing webhooks). Use a key with a higher role.

Retry strategy

Different status codes call for different retry behaviour:

  • 400 / 403 / 404 — Do not retry. Fix the request or the key.
  • 401 — Do not retry. Check your API key.
  • 429 — The call was not queued. Retry after the Retry-After header value (seconds).
  • 502 / 503 — Retry with exponential backoff (1s, 2s, 4s…), and send an Idempotency-Key so a retry can never place a second call.

Rate Limits & Paging

How much you can read, how to read it in pages, and what to do when you hit a limit. Applies to the results endpoints: Campaigns & Runs, Get Results and Get Summary.

The limits

LimitValueApplies to
Requests60 a minute per keyAll results endpoints together
Result rows500,000 a day per key (resets at 00:00 UTC)GET /campaigns/{id}/results
Rows per page250 (default 100)/results
Items per page200 (default 100)/campaigns and /runs
Query timeabout 20 seconds/results and /summary; slower queries are stopped
Scope of a readone run at a time/results and /summary need a run_id

These limits apply to each API key separately. Contact us if you need more. Calling a different endpoint, such as POST /calls, is limited separately by your trunk allocation (see Trigger a Call).

How the limits count

  • The request limit is a fixed one-minute window that starts on the clock minute. Every request counts, including ones that were refused with 429, so do not hammer the endpoint while waiting.
  • The row allowance is checked before a page is read and charged after it is sent, so the last page of the day can take you slightly over the 500,000.
  • Only /results rows count towards the daily allowance. /summary, /runs and /campaigns cost a request but no rows.

Response headers

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute (60).
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetWhen the current minute ends, in seconds since 1970 (Unix time).
X-Daily-Rows-RemainingResult rows left in today's allowance (on /results).
Retry-AfterOn a 429 or 503: seconds to wait before trying again.

When you hit a limit

Status / codeMeaningWhat to do
429 rate_limitedMore than 60 requests in the minute.Wait Retry-After seconds (never more than 60).
429 daily_quota_exceededToday's 500,000 rows are used up.Stop reading rows until 00:00 UTC. Next time use updated_since and read finished runs only once.
429 results_busyToo many result queries are running at once on our side.Wait Retry-After (about 5 seconds), then retry.
503 results_timeoutThe query took too long.Wait Retry-After, then retry with a smaller limit, a narrower from/to, or updated_since.

Always honour Retry-After. For anything else that is temporary (502, 503 without a code, a network error), retry with exponential backoff and a little random jitter, and give up after a few attempts. Do not retry 400, 401, 403 or 404: fix the request or the key.

Node.js — a request helper that respects the limits
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function getJson(url, apiKey, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
    } catch (err) {                                   // network error: retry
      if (attempt >= maxAttempts) throw err;
      await sleep(2 ** attempt * 500 + Math.random() * 250);
      continue;
    }
    if (res.ok) return res.json();

    const body = await res.json().catch(() => ({}));
    if (res.status === 429 || res.status === 503) {
      if (body.code === 'daily_quota_exceeded') throw new Error('Daily row allowance used up');
      if (attempt >= maxAttempts) throw new Error(`Gave up: ${res.status} ${body.code || ''}`);
      const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
      await sleep(wait * 1000 + Math.random() * 500);    // honour Retry-After
      continue;
    }
    if (res.status >= 500 && attempt < maxAttempts) {   // other temporary failure
      await sleep(2 ** attempt * 500 + Math.random() * 250);
      continue;
    }
    throw new Error(`${res.status}: ${body.error || 'request failed'}`);
  }
}

Paging

Lists are returned in pages. Every paged response has a pagination object:

JSON
"pagination": { "limit": 250, "has_more": true, "next_cursor": "eyJwIjoiKzkxOTg3NjU0MzIxMCJ9" }
  • Request the next page by sending next_cursor back as the cursor parameter, with the same run_id and filters. Keep going while has_more is true; on the last page next_cursor is null.
  • A cursor is an opaque token. Do not build, edit or store it long term. A malformed cursor returns 400.
  • /results is ordered by phone_number (up to 250 rows a page). /runs and /campaigns are ordered newest first (up to 200 a page).
  • Use limit to change the page size within the maximums above. A smaller page is a smaller, faster response.
Node.js — read every row of one run
async function readRun(campaignId, runId, apiKey, onRows) {
  const base = `https://api.audatec.in/integration/v1/campaigns/${campaignId}/results`;
  let cursor = null;
  do {
    const url = new URL(base);
    url.searchParams.set('run_id', runId);
    url.searchParams.set('limit', '250');
    if (cursor) url.searchParams.set('cursor', cursor);

    const page = await getJson(url.toString(), apiKey);   // retries and Retry-After handled above
    await onRows(page.results);

    cursor = page.pagination.has_more ? page.pagination.next_cursor : null;
    await sleep(1100);          // stay under 60 requests a minute
  } while (cursor);
}

Reading efficiently

  • Read a finished run once. A run whose status is completed rarely changes. Keep refreshing only the runs that are still in progress.
  • Fetch only what changed. Store the largest updated_at you have seen for each run and pass it as updated_since next time.
  • Use the summary for totals. Get Summary returns counts for a whole run in one request and uses no rows from your allowance.
  • Narrow by time. Add from and to to read only the calls placed in a period.
  • Plan your budget. The daily allowance is about 2,000 full pages of 250 rows. A run of 100,000 contacts is 400 pages and uses 100,000 of the 500,000 rows.
  • Do not poll tightly. Refreshing about once a minute is plenty; the data is calculated when you ask for it.

Call Status Values

A call has a status (where it is) and, once it has ended, an outcome (how it ended).

status

StatusTerminal?Meaning
queued● In progressCall accepted and being dispatched
ringing● In progressPhone is ringing at the recipient's end
in_progress● In progressRecipient picked up — AI conversation underway
ended✓ TerminalThe call is over. outcome says how it ended.

Status only moves forward: queued → ringing → in_progress → ended.

outcome

null until status is ended.

OutcomeEventMeaning
connectedcall.completedAnswered and the conversation ran. disposition is populated.
voicemailcall.failedThe call was answered by voicemail
busycall.failedThe line was busy
no_answercall.failedThe phone rang and nobody answered
declinedcall.failedThe recipient rejected the call
network_failedcall.failed / call.rejectedCarrier or network failure, the dialer could not start the call, or (call.rejected) your trunk allocation had no capacity when you triggered it
internal_error, rtc_error, connecthub_errorcall.failedA platform or telephony fault on our side
ℹ️ When is_terminal is true, status and outcome are final.

Error Codes

HTTP status codes returned by the API.

StatusNameWhen it occurs
200OKRequest succeeded. For POST /calls, "status": "duplicate" means an earlier request with the same Idempotency-Key already placed this call.
201CreatedCall accepted successfully, or a webhook subscription created
204No ContentA webhook subscription was deleted. There is no response body.
400Bad RequestInvalid request — missing field, wrong format, metadata mismatch. The error field explains exactly what is wrong.
401UnauthorizedAPI key is missing, invalid, or has been revoked
403ForbiddenThe key is valid but its role or scope does not allow this request ("code": "insufficient_scope"), for example a results key calling POST /calls, or a viewer key managing webhooks. See Results Access.
404Not FoundThe agent_id, sip_trunk_id, call_id or webhook id does not exist or does not belong to your org, or your org has no allocation on the trunk. On the results endpoints: the campaign does not exist, was deleted or belongs to another organisation (Campaign not found), or the run_id is not a run of that campaign (Run not found).
429Too Many RequestsTrunk allocation or rate limit reached. A call is not queued: wait the number of seconds in the Retry-After header, then retry. On the results endpoints the body carries a code: rate_limited (more than 60 requests in a minute), daily_quota_exceeded (the day's 500,000-row allowance is used up; resets at 00:00 UTC) or results_busy (too many result queries running at once).
502Bad GatewayThe Audatec dialer is temporarily unavailable or rejected the request. Retry with exponential backoff.
503Service UnavailableAuthentication service temporarily unavailable. Retry after a short delay. On the results endpoints, "code": "results_timeout" means the query took too long: wait the Retry-After seconds, then retry with a smaller limit, a narrower from/to, or updated_since.