AI calls, from your systems
https://api.agenticos.tech/api/v1/voice-agent · API fields are camelCase1. Overview
| You want to | Use | Authorised by |
|---|---|---|
| Let customers talk to the AI on your site or app | The hosted call page /v/<embed key> (link or iframe) | Embed key |
| Have the AI call a list of customers | Campaign API | JWT |
| Pull transcripts, summaries and captured data | Calls API | JWT |
| Have the AI answer your company number | Bind 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 for | Used for | From / where | Needed for |
|---|---|---|---|
| An integration user account | Logging in for a JWT (section 3). A dedicated email, admin role, email verified | Admin: Settings → Members → Invite | Outbound, results, numbers |
| The agent's name | Look up its id with GET /agents and use it in campaigns | Admin: Voice Agent → Agents | Outbound |
| The data you want after each call | Have the admin set the agent's Variables to Extract with the field names you agree on — they arrive in collected | Admin: agent → Variables to Extract | Results |
| The web-call link (embed key) | The iframe or WebView (section 2) | Admin: agent → Share → copy link / embed code | Web call |
| Web-call caps | Calls per hour per IP and call length (defaults 20 calls, 5 minutes) | Admin: Voice Agent settings | Web call |
| The number the AI answers | The number must be connected to AgenticOS's telephony first; then the admin binds it to an agent | AgenticOS team (connect) + admin (bind) | Inbound |
| The caller ID for outbound | What customers see when the AI calls; it can't be set freely — a route must be enabled | AgenticOS team (enable route) + admin (pick it in settings) | Outbound |
| Business hours | Outside them the bot says you're closed — start campaigns inside them | Admin: Voice Agent settings → Business hours | Outbound |
| An active plan | If the trial has ended, calls are refused | Organisation owner | Everything |
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 calls2. 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-keyreturns{ "embedKey": "emb_…", "url": "https://app.agenticos.tech/v/emb_…" }— calling again returns the same key
<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.
| What | iOS (WKWebView) | Android (WebView) |
|---|---|---|
| The app's microphone permission | NSMicrophoneUsageDescription | RECORD_AUDIO · MODIFY_AUDIO_SETTINGS plus the runtime request |
| Letting the page use the mic | iOS 15+: answer .grant in WKUIDelegate's requestMediaCapturePermissionFor | grant RESOURCE_AUDIO_CAPTURE in WebChromeClient.onPermissionRequest |
| Playing the AI's voice | allowsInlineMediaPlayback = true | mediaPlaybackRequiresUserGesture = false |
| Other | iOS 14.3 or later (WebRTC in WKWebView) | javaScriptEnabled = true · domStorageEnabled = true |
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"// 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/rotateissues 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
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.
- Create the campaign (draft)
- Add contacts
- Start it: set status to running
- Watch contact outcomes and read call results (section 5)
POST/api/v1/voice-agent/campaigns
{
"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
}| field | Details |
|---|---|
name | 1–200 characters (required) |
agentId | The agent who calls — required for the campaign to dial |
objective | The campaign's goal/script, ≤4000 characters, used to open every call |
maxConcurrent | Calls at once, 1–5 (default 2) |
status | draft · running · paused · done · scheduled |
POST/api/v1/voice-agent/campaigns/{id}/contacts
{
"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"}
GET/api/v1/voice-agent/campaigns/{id}/contacts
{
"contacts": [
{ "id": "…", "phone": "0812345678", "name": "Somchai", "status": "connected", "attempts": 1, "callId": "…", "updatedAt": "…" }
],
"counts": { "connected": 1, "no_answer": 1 }
}| Contact status | Meaning |
|---|---|
pending | Waiting to be dialled |
calling | Being dialled |
connected | Answered — results under callId |
no_answer | Not answered within 40 seconds |
failed | Busy or rejected |
voicemail | Reached voicemail |
- No automatic retries — retry with
POST /campaigns/{id}/contacts/retry-failed(all no_answer, failed, voicemail) orPOST /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}
{
"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" }
]
}| field | Meaning |
|---|---|
collected | Data the AI extracts when the call ends, per the agent's Variables to Extract (types string, number, boolean, date, email, phone) |
summary | Summary, key points, outcome, next action, sentiment and AI cost |
transcript | The conversation turn by turn; who is agent or customer |
recordingUrl | The recording; the link lasts 1 hour — call this endpoint again for a fresh one |
status | completed · 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}.
# 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 nowGET /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.
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
| What | Value |
|---|---|
| Web call page: calls per hour | Per organisation — default 20/hour per IP (0 = unlimited) |
| Web call page: call length | Default 5 minutes (30 s–60 min) |
| Campaign calls | Up to 10 minutes each |
| Concurrent calls per campaign | 1–5 |
| Ring time before no-answer | 40 seconds |
| Calls outside business hours | “We're closed” script, max 45 s |
| Contacts per request | 5,000 |
| Access token lifetime | 24 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