Developers · Voice Agent

AI calls, from your systems

Put a talk-to-AI button on your website or app, have the AI call customers through campaigns, and pull transcripts, summaries and captured data into your own systems.
Base URL https://api.agenticos.tech/api/v1/voice-agent · API fields are camelCase

1. Overview

You want toUseAuthorised by
Let customers talk to the AI on your site or appThe hosted call page /v/<embed key> (link or iframe)Embed key
Have the AI call a list of customersCampaign APIJWT
Pull transcripts, summaries and captured dataCalls APIJWT
Have the AI answer your company numberBind the number to an agent (Numbers)JWT

Before you start: what to ask the platform for

Some things come from the customer organisation's admin (owner or admin in AgenticOS); telephony pieces need the AgenticOS team. Collect what your integration uses before you start coding.

Ask forUsed forFrom / whereNeeded for
An integration user accountLogging in for a JWT (section 3). A dedicated email, admin role, email verifiedAdmin: Settings → Members → InviteOutbound, results, numbers
The agent's nameLook up its id with GET /agents and use it in campaignsAdmin: Voice Agent → AgentsOutbound
The data you want after each callHave the admin set the agent's Variables to Extract with the field names you agree on — they arrive in collectedAdmin: agent → Variables to ExtractResults
The web-call link (embed key)The iframe or WebView (section 2)Admin: agent → Share → copy link / embed codeWeb call
Web-call capsCalls per hour per IP and call length (defaults 20 calls, 5 minutes)Admin: Voice Agent settingsWeb call
The number the AI answersThe number must be connected to AgenticOS's telephony first; then the admin binds it to an agentAgenticOS team (connect) + admin (bind)Inbound
The caller ID for outboundWhat customers see when the AI calls; it can't be set freely — a route must be enabledAgenticOS team (enable route) + admin (pick it in settings)Outbound
Business hoursOutside them the bot says you're closed — start campaigns inside themAdmin: Voice Agent settings → Business hoursOutbound
An active planIf the trial has ended, calls are refusedOrganisation ownerEverything
The integration account's password is a secret: share it via a password manager and keep it on your server. If the user belongs to several organisations, ask which one and find its tenant id with GET /api/v1/auth/me. The embed key is public.
Request message (copy and send)
Hi — our dev team is connecting our systems to the AgenticOS voice agent. Could you provide:
1. An integration user with the admin role, email: integration@<company> — password via a password manager
2. The name of the agent to use for outbound / inbound calls
3. These Variables to Extract on that agent: customer_name, province, budget_thb, interested, callback_time
4. The agent's web-call link (Share → copy link), if we embed it on our site or app
5. Your business hours and the web-call caps you've set
6. (If the AI should answer calls) the number to use — the AgenticOS team connects it, plus the caller ID for outbound calls

2. A voice call on your website or app

Every agent has a hosted call page: open it and tap to talk — no WebRTC code on your side. Share it as a link, embed it as an iframe, or open it in your app's WebView.

Get the link

  • In the app: Voice Agent → pick the agent → Share → copy the link or embed code
  • Or through the API (owner/admin): POST /agents/{agent_id}/embed-key returns { "embedKey": "emb_…", "url": "https://app.agenticos.tech/v/emb_…" } — calling again returns the same key
HTML
<iframe
  src="https://app.agenticos.tech/v/emb_XXXXXXXX?theme=light"
  width="400" height="640"
  style="border:0;border-radius:16px"
  allow="microphone"
  title="Voice assistant"></iframe>
  • allow="microphone" is required, or the browser blocks the microphone
  • ?theme= dark · light · aurora · minimal (default dark)
  • The page shows a recording-consent notice before the call starts; its wording is currently Thai.

Inside a mobile app with a WebView

Load the /v/<embed key> link in a WebView. The extra work is letting the WebView use the microphone and play audio — skip it and the call button stays silent or reports a permission error.

WhatiOS (WKWebView)Android (WebView)
The app's microphone permissionNSMicrophoneUsageDescriptionRECORD_AUDIO · MODIFY_AUDIO_SETTINGS plus the runtime request
Letting the page use the miciOS 15+: answer .grant in WKUIDelegate's requestMediaCapturePermissionForgrant RESOURCE_AUDIO_CAPTURE in WebChromeClient.onPermissionRequest
Playing the AI's voiceallowsInlineMediaPlayback = truemediaPlaybackRequiresUserGesture = false
OtheriOS 14.3 or later (WebRTC in WKWebView)javaScriptEnabled = true · domStorageEnabled = true
Swift
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.load(URLRequest(url: URL(string: "https://app.agenticos.tech/v/emb_XXXXXXXX?theme=light")!))

// iOS 15+: let the call page use the microphone (only for our host)
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin,
             initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType,
             decisionHandler: @escaping (WKPermissionDecision) -> Void) {
    decisionHandler(origin.host.hasSuffix("agenticos.tech") ? .grant : .deny)
}
// Info.plist: NSMicrophoneUsageDescription = "ใช้ไมโครโฟนเพื่อคุยกับผู้ช่วย AI"
Kotlin
// AndroidManifest.xml
// <uses-permission android:name="android.permission.RECORD_AUDIO" />
// <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

// Ask RECORD_AUDIO at runtime first (ActivityResultContracts.RequestPermission), then:
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest) {
        val ours = request.origin.host?.endsWith("agenticos.tech") == true
        if (ours && PermissionRequest.RESOURCE_AUDIO_CAPTURE in request.resources) {
            request.grant(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE))
        } else {
            request.deny()
        }
    }
}
webView.loadUrl("https://app.agenticos.tech/v/emb_XXXXXXXX?theme=light")
  • Flutter: use flutter_inappwebview and grant in onPermissionRequest, after the app's own mic permission.
  • React Native: use react-native-webview, request the app's mic permission first, and on iOS set mediaCapturePermissionGrantType="grant".
  • Customer details (a name or member id) can't be passed into the call through the link yet — every call from this page is anonymous.

Keeping the link safe

  • The embed key is public — anyone with the link can call — so calls are capped by rate and length (section 8).
  • Leaked or abused: POST /agents/{agent_id}/embed-key/rotate issues a new key and the old link stops working at once
  • Rotating never touches the agent, its call history or its numbers.

3. Authentication for the management API (JWT)

The outbound, results and numbers APIs use a user's access token (JWT) — there are no separate API keys yet. Create a dedicated integration user (admin role) and have your server log in as it.

POST/api/v1/auth/login

shell
curl -s https://api.agenticos.tech/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"••••••••"}'

# → { "access_token": "eyJ…", "token_type": "bearer" }
  • Send it on every request: Authorization: Bearer <access_token>
  • Tokens last 24 hours and there is no refresh token — log in again on a 401.
  • A user in several organisations gets a token for the first one; switch with POST /api/v1/auth/switch-tenant {"tenant_id": "<uuid>"}
  • Keep the password and token on your server — never in an app or web page.

Access: reading needs access to the Voice Agent module; creating or changing agents, campaigns, numbers and contacts needs owner or admin.

4. Outbound AI calls through campaigns

The AI dials out through campaigns only; to call one person, make a one-contact campaign. While a campaign is running, the dialer picks up contacts every ~8 seconds.

  1. Create the campaign (draft)
  2. Add contacts
  3. Start it: set status to running
  4. Watch contact outcomes and read call results (section 5)

POST/api/v1/voice-agent/campaigns

request
{
  "name": "Follow up · September leads",
  "agentId": "<voice agent uuid>",
  "objective": "Call people who asked about the starter package. Ask permission for 2 minutes, answer questions, and book a site survey.",
  "maxConcurrent": 1
}
fieldDetails
name1–200 characters (required)
agentIdThe agent who calls — required for the campaign to dial
objectiveThe campaign's goal/script, ≤4000 characters, used to open every call
maxConcurrentCalls at once, 1–5 (default 2)
statusdraft · running · paused · done · scheduled

POST/api/v1/voice-agent/campaigns/{id}/contacts

request
{
  "contacts": [
    { "phone": "0812345678", "name": "Somchai" },
    { "phone": "+66898765432", "name": "Malee" }
  ]
}

# → 201 { "added": 2, "skipped": 0, "total": 2 }
  • Up to 5,000 per request; duplicates (in the campaign or the batch) are skipped.
  • Phone is 3–32 characters, kept as digits with an optional leading +; +66/66 numbers are dialled as 0….
  • There's no CSV upload in the API — convert to JSON first.

PATCH/api/v1/voice-agent/campaigns/{id}

Start and pause with {"status": "running"} / {"status": "paused"}

scheduled and startAt don't start anything yet — a campaign dials only once it is running. The dialer also doesn't check business hours: a call placed after hours gets the “we're closed” script. Start campaigns during business hours.

GET/api/v1/voice-agent/campaigns/{id}/contacts

200
{
  "contacts": [
    { "id": "…", "phone": "0812345678", "name": "Somchai", "status": "connected", "attempts": 1, "callId": "…", "updatedAt": "…" }
  ],
  "counts": { "connected": 1, "no_answer": 1 }
}
Contact statusMeaning
pendingWaiting to be dialled
callingBeing dialled
connectedAnswered — results under callId
no_answerNot answered within 40 seconds
failedBusy or rejected
voicemailReached voicemail
  • No automatic retries — retry with POST /campaigns/{id}/contacts/retry-failed (all no_answer, failed, voicemail) or POST /campaigns/{id}/contacts/{contact_id}/retry
  • A campaign turns done by itself when nothing is left to dial.

5. Call results: transcript, summary and captured data

GET/api/v1/voice-agent/calls/{id}

200
{
  "id": "c9a3cb03-…",
  "direction": "outbound",
  "channel": "phone",
  "status": "completed",
  "phone": "0812345678",
  "agentId": "…",
  "startedAt": "2026-09-30T03:51:42Z",
  "durationSec": 209,
  "intent": "booked_site_survey",
  "summary": {
    "text": "The customer is interested in the starter package…",
    "keyPoints": ["Has 40 m² in Nakhon Pathom", "Budget about 1M"],
    "outcome": "booked_site_survey",
    "nextAction": "Sales to call back Friday morning",
    "sentiment": "positive",
    "usage": { "totalThb": 4.82, "breakdown": [ … ] }
  },
  "collected": { "province": "Nakhon Pathom", "space_sqm": 40, "budget_thb": 1000000, "consent": true },
  "recordingUrl": "https://…(valid for 1 hour)",
  "transcript": [
    { "seq": 1, "who": "agent", "text": "Hello, this is …", "atOffset": "00:01" },
    { "seq": 2, "who": "customer", "text": "Yes, go ahead", "atOffset": "00:06" }
  ]
}
fieldMeaning
collectedData the AI extracts when the call ends, per the agent's Variables to Extract (types string, number, boolean, date, email, phone)
summarySummary, key points, outcome, next action, sentiment and AI cost
transcriptThe conversation turn by turn; who is agent or customer
recordingUrlThe recording; the link lasts 1 hour — call this endpoint again for a fresh one
statuscompleted · voicemail

GET/api/v1/voice-agent/collected

Only the calls with captured data: [{ "callId", "agentId", "caller", "phone", "channel", "startedAt", "collected" }]

GET/api/v1/voice-agent/calls

Every call, newest first. There's no paging or filtering yet: remember which ids you've processed and fetch details with /calls/{id}.

There's no call-ended webhook yet. Poll on a schedule — say every 1–5 minutes: GET /collected or GET /calls, then fetch only the new ids.
Python
# Python — pull new captured leads every few minutes
import requests
H = {"Authorization": f"Bearer {token}"}
rows = requests.get("https://api.agenticos.tech/api/v1/voice-agent/collected", headers=H, timeout=30).json()
for r in rows:
    if r["callId"] in seen:
        continue
    save_lead(r["phone"], r["collected"])   # your CRM
    seen.add(r["callId"])

6. Calls in progress

  • GET /live — calls happening now
  • GET /live/stream — Server-Sent Events: snapshot, start, turn (each spoken line), end; a ping every 15 s
  • A browser EventSource can't send the Authorization header — use fetch and read the stream.

7. Numbers and inbound calls

To have the AI answer your company number, add the number and bind it to an agent. The number must already be connected to AgenticOS's telephony (ask our team) — adding it through the API doesn't provision a line.

request
POST https://api.agenticos.tech/api/v1/voice-agent/numbers
{ "label": "Head office", "number": "021234567", "agentId": "<voice agent uuid>", "status": "connected" }
  • Inbound calls are matched on the number's digits (dashes and spaces ignored).
  • Change the answering agent: PATCH /numbers/{id} {"agentId": "…"}
  • The caller ID on outbound calls is picked from the routes we enable (GET /settings → availableOutboundRoutes); an arbitrary caller ID can't be set.

8. Limits

WhatValue
Web call page: calls per hourPer organisation — default 20/hour per IP (0 = unlimited)
Web call page: call lengthDefault 5 minutes (30 s–60 min)
Campaign callsUp to 10 minutes each
Concurrent calls per campaign1–5
Ring time before no-answer40 seconds
Calls outside business hours“We're closed” script, max 45 s
Contacts per request5,000
Access token lifetime24 hours

9. Not available yet

Listed so you can plan around them — tell our team which ones you need.

  • Server API keys (today: login + JWT)
  • Webhooks on call end or captured data (today: polling)
  • A call-now endpoint for one number (today: a one-contact campaign)
  • Paging and filters on /calls
  • Passing customer context into a web call through the link (today: ?theme= only)
  • Scheduled campaign starts and automatic retries

Every endpoint is in Swagger: https://api.agenticos.tech/docs