Integration guide · API, webhooks and phone line
Xura Global AI Voice Agents
How to connect Xura Global AI Voice Agents to your organization: your CRM, your phone line and, if you want, WhatsApp. Written for the technical team that carries out the integration.
Overview
Xura Global AI Voice Agents make and answer phone calls for your organization in natural spoken Sinhala. Each agent knows who it is talking to from your CRM, follows the call profile agreed with your team, answers questions from your own documents, and sends the result of every call back to your systems.
Every organization connects in the same way: three integration points link your systems with the agents, and calls travel over your own phone line.
| # | Connection | Direction | Set up by | Used for |
|---|---|---|---|---|
| 1 | Contact API | Xura Global → your systems | Your team | Knowing who the agent is talking to, and the facts for the call |
| 2 | Webhook receiver | Xura Global → your systems | Your team | Receiving the result of every call: outcome, captured data, summary and transcript |
| 3 | Voice API | Your systems → Xura Global | Xura Global | Starting calls and campaigns, managing knowledge documents, reading results |
| 4 | Phone line (SIP trunk) | Both ways | Your team, your operator and Xura Global | Carrying calls to and from your phone numbers |
| + | WhatsApp calling | Both ways | Your team and Xura Global | Optional: calls with your customers on WhatsApp |
What organizations use it for
The integration is the same for all of them. Only the call profile is specific to your organization: what the agent says, what it needs to know and what it records. Each organization also gets its own dedicated system and pays AI and phone usage directly to Google and its telecom operator (see your system and costs).
Integration process
Every project follows the same five phases. Steps marked Mandatory are needed before go-live; the others depend on how you use the agents.
-
1
Plan
Agree the call profilesBoth teamsMandatoryDecide what each kind of call should achieve, what the agent may say, which contact fields it needs and which outcomes you want back. Call profiles
Order the phone lineYour teamYour operatorMandatoryOrder a SIP trunk and phone numbers from a licensed telecom operator and choose how it connects (option A, B or C). Start early: operator provisioning is often the longest step. Phone line
Set up your accountsYour teamMandatoryCreate a Google Cloud project with billing (for Gemini and Speech-to-Text) and decide where your system runs: on a server in your name or hosted by Xura Global. Your system and costs
Exchange addresses and keysBoth teamsMandatoryBase URLs, API keys, the webhook signing secret and firewall addresses, for testing and for production. Onboarding checklist
-
2
Build
Build the Contact API ①Your teamMandatoryTwo read-only endpoints that return a contact and the facts for the call. Needed for inbound calls and campaigns. Contact API
Build the webhook receiver ②Your teamMandatoryOne HTTPS endpoint that verifies and stores the result of every call. Webhook receiver
Connect your systems to the Voice API ③Your teamFor outbound callsStart calls and campaigns from your systems and show results in your own front end. Not needed if the agents only answer calls. Voice API
Prepare knowledge documentsYour teamRecommendedProducts, services, procedures, fees, policies and frequent questions the agents may answer from. Knowledge base
Install and configure your systemXura GlobalMandatoryXura Global installs your dedicated system, connects it to your Google Cloud project, sets up your call profiles, voice and calling hours, and loads your knowledge documents.
-
3
Connect
Connect the phone line ④Your teamYour operatorXura GlobalMandatoryConnect the SIP trunk to the Xura Global gateway and make test calls in both directions. Phone line
Connect WhatsApp callingYour teamXura GlobalOptionalLet customers call you on WhatsApp, and call them there once they allow it. WhatsApp calling
-
4
Test
Run the acceptance testsBoth teamsMandatoryTests of data, results, calls, knowledge and campaigns, run together in the testing environment. Testing and go-live
-
5
Go live
PilotBoth teamsMandatoryStart with a small group of contacts. Review transcripts and outcomes together and adjust the profiles.
Roll out and monitorBoth teamsMandatorySwitch to production keys and numbers. Failed webhooks, Contact API errors and phone line faults alert both teams.
Who does what
| Area | Xura Global | Your team |
|---|---|---|
| AI voice agent software and call handling | Builds and maintains it | — |
| Your dedicated system (server with the agents, gateway and database) | Installs, updates and monitors it | Provides a server in your name, or uses hosting by Xura Global |
| AI services (Google Cloud: Gemini and Speech-to-Text) | Connects them to your system | Owns the Google Cloud project and pays for usage |
| Call profiles | Configures and tests them | Defines the goals and rules, and approves them |
| Contact data (the source of truth) | Reads it through ① | Owns it and builds ① Contact API |
| Call results | Sends them through ② | Builds ② Webhook receiver, stores the results and acts on them |
| Front end for your staff (call lists, results, reports) | Provides ③ Voice API | Builds the front end on ③ |
| Phone numbers and SIP trunk ④ | Connects the trunk to the gateway | Orders them from a licensed operator and holds the contract |
| WhatsApp calling (optional) | Connects WhatsApp calls to the agents | Holds the WhatsApp Business account and number |
| Knowledge base content | Hosts and indexes it | Writes the documents and keeps them up to date |
| Follow-up actions (call-backs, reminders, opt-outs) | Flags them in each result | Carries them out |
The Xura Global dashboard used in demos is an internal tool for testing and support. Your staff use your own front end, built on the Voice API.
Your system and running costs
Every organization gets its own dedicated system: its own server (or its own isolated Docker installation), database, telephony gateway and API keys. Nothing is shared with other organizations, and usage is billed directly to your own accounts.
| Cost | Paid by | Paid to |
|---|---|---|
| AI usage: Gemini and Speech-to-Text | Your organization | Google Cloud, through your own project |
| Phone numbers and calls (SIP trunk) | Your organization | Your telecom operator |
| WhatsApp calls (optional) | Your organization | Meta, through your WhatsApp Business account |
| Server | Your organization | Your hosting provider, or Xura Global if it hosts your system |
| Setup, and the optional monthly maintenance plan | Your organization | Xura Global |
- Maintenance plan (optional). Software updates, security patches, monitoring, backups and support. Without a plan, Xura Global helps on request.
- Budget alert. Set a monthly budget alert in your Google Cloud project so AI costs never surprise you.
- Keep billing enabled. Under Google's terms for paid use, the Gemini API does not use your call data to improve Google's products, and Speech-to-Text does so only if you opt in to data logging.
Call profiles
A call profile describes one kind of call: its purpose, what the agent says and checks, the contact data it needs and the outcomes it records. Your team and Xura Global agree each profile at the start of the project; Xura Global configures it and you approve it with test calls. An organization can have several profiles, for example a payment reminder for outbound calls and a customer service line for inbound calls.
What a profile defines
- PurposeThe goal of the call, in one sentence.
- DirectionOutbound, inbound or both, and on which numbers.
- GreetingHow the agent introduces itself and your organization.
- Identity checkWhat the agent confirms before it discusses any detail.
- Context fieldsThe facts the agent needs from your Contact API.
- Outcomes and captured dataThe result codes and values you want back.
- Rules and limitsWhat the agent may agree to, and what it must never say.
- HandoverWhen the agent promises a call back from your staff.
- Calling hours and retriesHours, days, holidays, attempts and the gap between attempts.
- Voice and knowledgeThe agent's voice and the documents it may answer from.
Example profiles
Starting points that are adapted to each organization. The meaning of every code is in Reference codes.
| Profile | Typical organizations | Context fields | Outcome codes |
|---|---|---|---|
Payment reminderpayment-reminder | Banks, finance and leasing companies, telcos, utilities, insurers, schools | amountDue, currency, dueDate | PROMISE_TO_PAY, ALREADY_PAID, FINANCIAL_HARDSHIP, REFUSED_TO_PAY, DISPUTED |
Appointment reminderappointment-reminder | Hospitals, clinics, laboratories, service centres | appointmentAt, withWhom, location | CONFIRMED, RESCHEDULE_REQUESTED, CANCELLED |
Order and delivery confirmationdelivery-confirmation | Online shops, couriers, distributors | orderNo, deliveryDate, codAmount | CONFIRMED, CHANGE_REQUESTED, CANCELLED |
Lead follow-uplead-follow-up | Real estate, vehicle sales, education, insurance | product, enquiryDate | INTERESTED, NOT_INTERESTED |
Customer service linecustomer-service | Any organization with a hotline (inbound) | Optional account summary | RESOLVED |
Feedback surveyfeedback-survey | Any organization | serviceDate, serviceType | SURVEY_COMPLETED, DECLINED |
Every profile also uses the standard outcome codes, such as CALLBACK_REQUESTED, OPT_OUT and NO_ANSWER.
How the agents behave on every call
These rules are built into every agent and apply to all profiles. Your profile can add to them but not remove them.
- The agent introduces itself with your organization's name, and says it is an automated assistant when asked.
- It confirms it is speaking to the right person before discussing any personal detail, and never discusses a contact's details with anyone else.
- It answers only from your contact data and your knowledge base, and never invents figures, dates or policies. What it cannot answer becomes a call-back request.
- It stays polite and never threatens, pressures or argues.
- It reads important details (dates, times, amounts) back and waits for the contact to confirm them.
- When a contact asks not to be called again, it ends the call politely and reports
OPT_OUT. - It never calls contacts marked
doNotCall, and campaigns call only within your calling hours. A call ends after 10 minutes at most.
Changing a profile. Tell Xura Global what should change. Wording and rules change after a test call; new context fields or outcome codes are agreed with your team first because they change the data you send or receive.
How a call flows
Outbound call
Your system asks the agents to call one contact. The agent fetches fresh data from your Contact API before it dials, so it never uses old figures.
sequenceDiagram
autonumber
participant Y as Your system
participant X as Xura Voice API
participant C as ① Contact API
participant P as Contact's phone
participant W as ② Webhook
Y->>X: POST /v1/calls
X->>C: GET /contacts/{id}
C-->>X: 200 contact
X-->>Y: 202 queued
X->>P: Rings the phone (④)
P-->>X: Answers
X->>W: call.started
Note over X,P: Conversation in Sinhala
X->>W: call.completed
W-->>X: 200 OK
Inbound call
A contact calls one of your numbers. The agent looks the caller up by phone number while it greets them.
sequenceDiagram
autonumber
participant P as Contact's phone
participant X as Xura AI Voice Agent
participant C as ① Contact API
participant W as ② Webhook
P->>X: Calls your number (④ or WhatsApp)
X->>C: GET /contacts/lookup?phone=…
C-->>X: 200 contact, or 404 unknown
X->>W: call.started
Note over P,X: Agent confirms the name, then helps
X->>W: call.completed
Campaign
A campaign calls a list of contacts automatically, one after another, only within the calling hours you set. Unanswered calls are retried.
sequenceDiagram
participant Y as Your system
participant X as Xura Voice API
participant W as ② Webhook
Y->>X: POST /v1/campaigns
X-->>Y: 201 running
loop Each contact, within calling hours
X->>X: Fetch contact (①) and dial
X->>W: call.completed or call.not_connected
end
Note over X: No answer or busy:<br/>retried after retryMinutes,<br/>up to maxAttempts
X->>W: campaign.completed
Conventions
These rules apply to ①, ② and ③.
Content-Type: application/json; charset=utf-8). Sinhala text is sent as normal UTF-8 characters.+94771234567. In a query string the + is URL-encoded as %2B.2026-09-25T04:44:50Z (10:14:50 in Sri Lanka). You may send any offset, for example 2026-10-02T09:30:00+05:30.YYYY-MM-DD in your organization's time zone (Asia/Colombo unless agreed otherwise), for example a due date or a promised date.5000 and "LKR". No thousands separators.payment-reminder.Built by your team1Contact API
A small read-only API in front of your CRM. The agents call it to find out who they are talking to and what the call is about. It has two endpoints and returns one object, the contact.
https://api.your-company.com/xura/v1. Send us one for testing and one for production.Authorization: Bearer <key>. Please rotate it at least every 90 days./contacts/lookup?phone={e164}YOUR TEAMFind a contact by phone number. We call this for every inbound call, on the phone line and on WhatsApp, as soon as the call connects.
| Query parameter | Type | Description |
|---|---|---|
phone | string | The caller's number in E.164, URL-encoded: %2B94771234567. |
- 200 with the contact object when the number belongs to a contact.
- 404 when it does not. The agent treats the caller as unknown: it takes their name and reason and records a call-back request.
- If several contacts share the number (a family phone, for example), return the one your staff would want the agent to talk about first, such as the one with the most urgent open item. The agent confirms the name before discussing anything.
GET /xura/v1/contacts/lookup?phone=%2B94771234567 HTTP/1.1
Host: api.your-company.com
Authorization: Bearer ck_live_4f7c9b1e…
Accept: application/json
{ "error": { "code": "not_found", "message": "No contact uses this phone number." } }
/contacts/{contactId}YOUR TEAMGet a contact by your ID. We call this when a call is requested through the Voice API, and before every campaign attempt, so the agent uses today's data.
- 200 with the contact object.
- 404 when the ID does not exist. The call is not placed (see errors and time limits).
{
"contactId": "C-10042",
"name": "Nimal Perera",
"spokenName": "නිමල්",
"salutation": "මහත්තයා",
"phone": "+94771234567",
"language": "si",
"doNotCall": false,
"context": {
"amountDue": 5000,
"currency": "LKR",
"dueDate": "2026-08-10"
}
}
Contact object
Send only these fields. The agents do not need, and should not receive, national ID numbers, addresses or full account histories.
| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | Yes | Your ID for the person the agent talks to. It comes back in every event about this contact. |
name | string | Yes | Full name as in your records. |
spokenName | string | Recommended | The name in Sinhala script, the way the agent should say it (නිමල්). Without it the agent reads name. |
salutation | string | Recommended | How the agent addresses the contact: මහත්තයා, මහත්මිය, මෙනවිය, or empty. |
phone | string | Yes | The number to call, in E.164. |
language | string | No | Language of the call. si (Sinhala) is the default; other languages are enabled per project. |
doNotCall | boolean | No | true: the agents never call this contact. Their own calls are still answered. Default false. |
context | object | Per profile | The facts the agent may use on this call. Field names and types are agreed in the call profile. Up to 30 fields and 4 KB. |
Context examples
{ "amountDue": 5000, "currency": "LKR", "dueDate": "2026-08-10" }
{ "appointmentAt": "2026-10-02T09:30:00+05:30", "withWhom": "Dr. Perera", "location": "Kandy branch" }
{ "orderNo": "ORD-55812", "deliveryDate": "2026-09-27", "codAmount": 12500, "currency": "LKR" }
- Use the formats in Conventions: numbers for amounts,
YYYY-MM-DDfor dates, ISO 8601 for times. - Include only what the agent needs to say or check. It uses context only after the identity check.
- Never include passwords, PINs, full card or account numbers, national ID numbers or detailed medical information.
Errors and time limits
| Your response | Inbound call | Outbound call |
|---|---|---|
| 200 within 2 s | The agent uses the data | The call is placed |
200 with doNotCall: true | Answered as normal | Not placed. Voice API: 422 contact_not_callable. Campaign: contact skipped (CANCELED) |
| 404 | Caller treated as unknown | Not placed. Voice API: 404 contact_not_found. Campaign: contact skipped (FAILED) |
| 401 / 403 | Caller treated as unknown; Xura Global is alerted | Not placed. Voice API: 502 contact_api_error. Campaign: tried again at the next attempt |
| 429 / 5xx or no answer in 2 s | We retry once after 300 ms, then treat the caller as unknown | We retry once, then as for 401 / 403 |
Built by your team2Webhook receiver
One HTTPS endpoint on your side. We send it an event every time something happens to a call or a campaign. Your endpoint must:
- Accept
POSTrequests with a JSON body at a public HTTPS URL, for examplehttps://api.your-company.com/webhooks/xura. - Check the signature on every request and reject requests that fail it.
- Answer with a 2xx status within 10 seconds. Save the event first and do the slow work (CRM updates, SMS) afterwards.
- Process each event once. The same event can arrive more than once, so skip any
idyou have already stored. - Not rely on arrival order. Use
occurredAtto order events for the same call.
Event types
| Event | Sent when | Main content |
|---|---|---|
call.started | A contact answers our call, or the agent answers theirs | Call |
call.completed | An answered call has ended and been analysed (usually 2–5 s after hang-up) | Call, outcome, transcript |
call.not_connected | An outbound call was not answered, was busy, failed, was cancelled or could not be placed | Call, outcome, campaign retry plan |
campaign.completed | Every contact in a campaign has a final result, or the campaign was stopped | Campaign totals |
webhook.test | A test is requested with POST /v1/webhook/test | A test message |
Envelope
Every event has the same outer shape:
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID, for example evt_5f0c2b7e9d8a4c21. Use it to skip duplicates. |
type | string | One of the event types above. |
occurredAt | timestamp | When it happened. |
apiVersion | string | v1. |
data | object | The content. See payload examples and call and outcome fields. |
Headers we send
| Header | Example |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | XuraGlobal-Webhooks/1.0 |
X-Xura-Event-Id | evt_5f0c2b7e9d8a4c21 |
X-Xura-Event-Type | call.completed |
X-Xura-Signature | t=1790311541,v1=6b1d8f…c02a |
Verifying signatures
We sign every request with a secret that only you and Xura Global know (the webhook signing secret, starting with whsec_,
shared at onboarding). Checking it proves that the request came from Xura Global and was not changed on the way.
- Read
t(Unix time in seconds) andv1(the signature) from theX-Xura-Signatureheader. - Compute HMAC-SHA256 of
t + "." + raw request bodywith the secret, as lowercase hex. Use the raw bytes exactly as received, before any JSON parsing. - Compare it with
v1using a constant-time comparison. - Reject the request with 401 if the signatures differ or if
tis more than 5 minutes away from your clock.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.XURA_WEBHOOK_SECRET; // whsec_…
app.post('/webhooks/xura', express.raw({ type: 'application/json' }), (req, res) => {
const parts = Object.fromEntries(
(req.get('X-Xura-Signature') ?? '').split(',').map((p) => p.split('=')),
);
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${parts.t}.`)
.update(req.body) // raw bytes
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
const valid =
parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(401);
const event = JSON.parse(req.body);
saveEvent(event); // store it, then process asynchronously (skip if event.id is already stored)
res.sendStatus(200);
});
import hashlib, hmac, json, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["XURA_WEBHOOK_SECRET"].encode() # whsec_…
@app.post("/webhooks/xura")
def xura_webhook():
header = request.headers.get("X-Xura-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", "0"), parts.get("v1", "")
body = request.get_data() # raw bytes
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(t)) > 300 or not hmac.compare_digest(v1, expected):
abort(401)
event = json.loads(body)
save_event(event) # store it, then process asynchronously (skip duplicate event ids)
return "", 200
@PostMapping("/webhooks/xura")
public ResponseEntity<Void> xuraWebhook(
@RequestHeader("X-Xura-Signature") String signature,
@RequestBody byte[] body) throws Exception {
Map<String, String> parts = Arrays.stream(signature.split(","))
.map(p -> p.split("=", 2)).filter(p -> p.length == 2)
.collect(Collectors.toMap(p -> p[0], p -> p[1]));
long t = Long.parseLong(parts.getOrDefault("t", "0"));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
mac.update((t + ".").getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(mac.doFinal(body)); // raw bytes
boolean fresh = Math.abs(Instant.now().getEpochSecond() - t) <= 300;
boolean valid = MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
parts.getOrDefault("v1", "").getBytes(StandardCharsets.UTF_8));
if (!fresh || !valid) return ResponseEntity.status(401).build();
events.save(body); // store it, then process asynchronously (skip duplicate event ids)
return ResponseEntity.ok().build();
}
$secret = getenv('XURA_WEBHOOK_SECRET'); // whsec_…
$body = file_get_contents('php://input'); // raw bytes
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_XURA_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '0') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($sig['t'] ?? 0)) <= 300;
if (!$fresh || !hash_equals($expected, $sig['v1'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
save_event($event); // store it, then process asynchronously (skip duplicate event ids)
http_response_code(200);
Retries
If your endpoint does not answer with a 2xx within 10 seconds, we send the same event again (same id):
| Attempt | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Sent after | at once | 1 min | 5 min | 30 min | 2 h | 6 h |
After the sixth attempt the event is marked as failed and both teams are alerted. The result is not lost: you can always read it with
GET /v1/calls/{callId}, and we can send failed events again once your endpoint is back.
Payload examples
call.completed
The outcome.data values depend on the call profile. Two examples:
{
"id": "evt_5f0c2b7e9d8a4c21",
"type": "call.completed",
"occurredAt": "2026-09-25T04:45:41Z",
"apiVersion": "v1",
"data": {
"call": {
"id": "3ec2bcda-8289-4a84-90bc-46fc2f80f77a",
"direction": "outbound",
"channel": "phone",
"profile": "payment-reminder",
"status": "completed",
"contactId": "C-10042",
"phone": "+94771234567",
"externalRef": "INV-2026-08-00123",
"metadata": { "branch": "Kandy" },
"campaign": null,
"startedAt": "2026-09-25T04:44:50Z",
"endedAt": "2026-09-25T04:45:38Z",
"durationSec": 48,
"endReason": "agent_completed"
},
"outcome": {
"code": "PROMISE_TO_PAY",
"summary": "Nimal promised to pay the overdue LKR 5,000 on 2 October 2026. The agent read the date back and he confirmed it.",
"sentiment": "POSITIVE",
"data": { "promisedDate": "2026-10-02", "promisedAmount": 5000, "currency": "LKR" },
"followUp": null
},
"transcript": [
{ "role": "agent", "text": "ආයුබෝවන්! මම ඒබීසී සමාගමෙන් කතා කරන්නේ. මට නිමල් මහත්තයා එක්ක කතා කරන්න පුළුවන්ද?", "at": "2026-09-25T04:44:51Z" },
{ "role": "contact", "text": "ඔව්, නිමල් කතා කරන්නේ.", "at": "2026-09-25T04:44:58Z" },
{ "role": "agent", "text": "නිමල් මහත්තයා, අගෝස්තු දහවෙනිදා ගෙවන්න තිබුණ රුපියල් පන්දාහක මුදලක් තවම ගෙවලා නැහැ. ඒක කවදා ගෙවන්න පුළුවන්ද?", "at": "2026-09-25T04:45:00Z" },
{ "role": "contact", "text": "මම ලබන සිකුරාදා ගෙවන්නම්.", "at": "2026-09-25T04:45:21Z" },
{ "role": "agent", "text": "එතකොට ඔක්තෝබර් දෙවෙනිදා රුපියල් පන්දාහ, හරිද?", "at": "2026-09-25T04:45:23Z" },
{ "role": "contact", "text": "ඔව්, හරි.", "at": "2026-09-25T04:45:31Z" },
{ "role": "agent", "text": "ස්තූතියි නිමල් මහත්තයා. සුභ දවසක්!", "at": "2026-09-25T04:45:33Z" }
]
}
}
{
"id": "evt_2b9e71c4d05a8f36",
"type": "call.completed",
"occurredAt": "2026-09-25T05:31:12Z",
"apiVersion": "v1",
"data": {
"call": {
"id": "8d4a0f6e-2c31-4b7a-9e15-6f0b3c8d2a91",
"direction": "outbound",
"channel": "phone",
"profile": "appointment-reminder",
"status": "completed",
"contactId": "P-2291",
"phone": "+94712345678",
"externalRef": "APT-77120",
"metadata": null,
"campaign": { "id": "CAMP-6D2E91AF", "attempt": 1, "willRetry": false, "nextAttemptAt": null },
"startedAt": "2026-09-25T05:30:14Z",
"endedAt": "2026-09-25T05:31:09Z",
"durationSec": 55,
"endReason": "agent_completed"
},
"outcome": {
"code": "RESCHEDULE_REQUESTED",
"summary": "Sunitha cannot come on 2 October and asked to move the appointment to Monday 5 October. Staff should confirm a time.",
"sentiment": "NEUTRAL",
"data": { "requestedDate": "2026-10-05" },
"followUp": { "reason": "Confirm a new appointment time on 5 October 2026.", "preferredTime": null }
},
"transcript": [
{ "role": "agent", "text": "ආයුබෝවන්! මම ඒබීසී මෙඩිකල් සෙන්ටර් එකෙන් කතා කරන්නේ. සුනීතා මහත්මිය එක්කද මම කතා කරන්නේ?", "at": "2026-09-25T05:30:15Z" },
{ "role": "contact", "text": "ඔව්.", "at": "2026-09-25T05:30:21Z" },
{ "role": "agent", "text": "සුනීතා මහත්මිය, ඔබට ඔක්තෝබර් දෙවෙනිදා උදේ නවයයි තිහට වෛද්ය පෙරේරා මහතා හමුවෙන්න වේලාවක් වෙන් කරලා තියෙනවා. ඔබට එදාට එන්න පුළුවන්ද?", "at": "2026-09-25T05:30:23Z" },
{ "role": "contact", "text": "එදාට බැහැ, ඊළඟ සඳුදා පුළුවන්.", "at": "2026-09-25T05:30:40Z" },
{ "role": "agent", "text": "හරි, එහෙනම් ඔක්තෝබර් පස්වෙනිදා සඳුදාට වෙනස් කරන්න ඉල්ලීමක් දාන්නම්. අලුත් වේලාව තහවුරු කරන්න අපේ කාර්ය මණ්ඩලය ඔබට කතා කරයි.", "at": "2026-09-25T05:30:43Z" },
{ "role": "contact", "text": "හරි, ස්තූතියි.", "at": "2026-09-25T05:30:58Z" }
]
}
}
call.not_connected (a campaign attempt that will be retried)
{
"id": "evt_8a41d3c09b7e2f65",
"type": "call.not_connected",
"occurredAt": "2026-09-25T05:02:41Z",
"apiVersion": "v1",
"data": {
"call": {
"id": "b7f9e2c1-4d3a-4c8e-9f61-2a5d7e0c9b13",
"direction": "outbound",
"channel": "phone",
"profile": "appointment-reminder",
"status": "not_connected",
"contactId": "P-2307",
"phone": "+94763456789",
"externalRef": "APT-77134",
"metadata": null,
"campaign": { "id": "CAMP-6D2E91AF", "attempt": 1, "willRetry": true, "nextAttemptAt": "2026-09-25T06:32:41Z" },
"startedAt": "2026-09-25T05:02:10Z",
"endedAt": "2026-09-25T05:02:41Z",
"durationSec": 0,
"endReason": "no_answer"
},
"outcome": { "code": "NO_ANSWER", "summary": "The call was not answered.", "sentiment": null, "data": null, "followUp": null }
}
}
call.started
{
"id": "evt_1c7e4b92a0d35f8e",
"type": "call.started",
"occurredAt": "2026-09-25T05:20:03Z",
"apiVersion": "v1",
"data": {
"call": {
"id": "e41f7a2b-9c6d-4e1f-8b3a-5d2c9f0e7a64",
"direction": "inbound",
"channel": "phone",
"profile": "customer-service",
"status": "in_progress",
"contactId": "C-10042",
"phone": "+94771234567",
"startedAt": "2026-09-25T05:20:03Z"
}
}
}
campaign.completed
{
"id": "evt_d93b0e5f7c2a4186",
"type": "campaign.completed",
"occurredAt": "2026-09-25T11:30:05Z",
"apiVersion": "v1",
"data": {
"campaign": {
"id": "CAMP-6D2E91AF",
"name": "October appointment reminders",
"profile": "appointment-reminder",
"channel": "phone",
"status": "completed",
"startedAt": "2026-09-25T03:30:00Z",
"finishedAt": "2026-09-25T11:30:05Z",
"totals": { "contacts": 120, "reached": 94, "notReached": 21, "skipped": 5 },
"byOutcome": {
"CONFIRMED": 68,
"RESCHEDULE_REQUESTED": 13,
"CANCELLED": 4,
"CALLBACK_REQUESTED": 6,
"WRONG_PERSON": 3,
"NO_ANSWER": 17,
"BUSY": 4
}
}
}
}
Call and outcome fields
Call
| Field | Type | Description |
|---|---|---|
id | string | Call ID (UUID). |
direction | string | outbound (the agent called) or inbound (the contact called). |
channel | string | phone (your SIP trunk) or whatsapp. |
profile | string | The call profile used, for example payment-reminder. |
status | string | queued, dialing, in_progress, completed or not_connected. See call status. |
contactId | string or null | Your contact ID. null for an unknown inbound caller. |
phone | string | The contact's number (E.164). |
externalRef | string or null | Your own reference, if you sent one when starting the call. |
metadata | object or null | Your key–value data from the call request, returned unchanged. |
campaign | object or null | For campaign calls: id, attempt, willRetry, nextAttemptAt. |
startedAt | timestamp | When the call was answered or, for a call that was not connected, when dialling began. |
endedAt | timestamp | When the call ended. |
durationSec | integer | Talk time in seconds. 0 when not connected. |
endReason | string | Why the call ended. See end reasons. |
Outcome
| Field | Type | Description |
|---|---|---|
code | string | The result of the call: a standard code or a code of the call profile. |
summary | string | One or two sentences in English for your staff. |
sentiment | string or null | POSITIVE, NEUTRAL, NEGATIVE or HOSTILE. null when not connected. |
data | object or null | Values captured on the call, as defined in the call profile, for example promisedDate and promisedAmount. Every value was read back to the contact and confirmed. |
followUp | object or null | Set when your staff should act: reason and, if the contact gave one, preferredTime in their own words. |
Transcript
A list of { role, text, at } entries in call order. role is agent, contact or kb
(a knowledge-base lookup: text is the question looked up and sources lists the documents used). The contact's words
are transcribed automatically and can contain recognition errors; the outcome fields are the reliable record of what was agreed.
Provided by Xura Global3Voice API
Your back end calls this API to start calls and campaigns, read results and manage the knowledge base. Call it from your servers only: the API key must never be placed in a web page or a mobile app.
Basics
https://voice.your-company.com/v1. Test and live keys use the same address. Examples below use https://voice-api.example.com/v1.Authorization: Bearer xv_live_… (production) or xv_test_… (testing). The keys are created on your system and work only there. We send them securely at onboarding and rotate them on request.Idempotency-Key header (a UUID) with every POST that creates something. Repeating a request with the same key within 24 hours returns the first response and never places a second call.Retry-After header in seconds.GET /v1/profiles lists them with their context fields and outcome codes.?limit= (1–100, default 50) and ?cursor=. Responses are { "data": [...], "nextCursor": "…" }; nextCursor is null on the last page.| Method | Path | Purpose |
|---|---|---|
| POST | /v1/calls | Start an outbound call |
| GET | /v1/calls/{callId} | Get a call with its outcome and transcript |
| GET | /v1/calls | List calls |
| POST | /v1/calls/{callId}/cancel | Stop a ringing call or end a live call |
| POST | /v1/campaigns | Create a campaign |
| GET | /v1/campaigns | List campaigns |
| GET | /v1/campaigns/{campaignId} | Progress and per-contact results |
| POST | /v1/campaigns/{campaignId}/start · /pause · /stop | Control a campaign |
| GET | /v1/profiles | List your call profiles |
| GET | /v1/knowledge/documents | List knowledge documents |
| POST | /v1/knowledge/documents | Add a document |
| PUT | /v1/knowledge/documents/{documentId} | Edit a document, or switch it on or off |
| DELETE | /v1/knowledge/documents/{documentId} | Delete a document |
| POST | /v1/knowledge/ask | Preview the agent's answer to a question |
| GET PUT | /v1/webhook | Read or set your webhook URL |
| POST | /v1/webhook/test | Send a webhook.test event |
| GET PUT | /v1/settings | Agent voice and default calling hours |
| GET | /v1/events | Live call updates (optional) |
| GET | /v1/health | Service status (no key needed) |
Calls
/v1/callsXURA GLOBALStart an outbound call. We fetch the contact from your Contact API, check it can be called, and dial. The result arrives on your webhook.
| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | Yes* | Your contact ID. |
contact | object | No* | The full contact object, instead of contactId. We then do not call your Contact API for this call. |
profile | string | If several | The call profile, for example payment-reminder. Required when you have more than one outbound profile. |
channel | string | No | phone (default) or whatsapp (see WhatsApp calling). |
phone | string | No | Call this number instead of the contact's (E.164). |
externalRef | string | No | Your own reference (up to 64 characters), returned in every event about this call. |
metadata | object | No | Up to 20 string key–value pairs (1 KB), returned unchanged. |
* Send contactId or contact, not both.
curl https://voice-api.example.com/v1/calls \
-H "Authorization: Bearer xv_test_9b2e…" \
-H "Idempotency-Key: 6c1d2c1e-0f7a-4d52-9c1b-2f8e7a3d4b10" \
-H "Content-Type: application/json" \
-d '{ "contactId": "C-10042", "profile": "payment-reminder", "externalRef": "INV-2026-08-00123", "metadata": { "branch": "Kandy" } }'
{
"id": "3ec2bcda-8289-4a84-90bc-46fc2f80f77a",
"direction": "outbound",
"channel": "phone",
"profile": "payment-reminder",
"status": "queued",
"contactId": "C-10042",
"phone": "+94771234567",
"externalRef": "INV-2026-08-00123",
"createdAt": "2026-09-25T04:44:21Z"
}
{
"profile": "appointment-reminder",
"contact": {
"contactId": "P-2291",
"name": "Sunitha Fernando",
"spokenName": "සුනීතා",
"salutation": "මහත්මිය",
"phone": "+94712345678",
"context": { "appointmentAt": "2026-10-02T09:30:00+05:30", "withWhom": "Dr. Perera", "location": "Kandy branch" }
},
"externalRef": "APT-77120"
}
Refused with 422 contact_not_callable when the contact has doNotCall: true, and with 503 line_unavailable when the phone line is down.
/v1/calls/{callId}XURA GLOBALGet a call with the same call, outcome and transcript fields as the call.completed event. While a call is in progress, outcome is null and the transcript grows.
/v1/callsXURA GLOBALList calls, newest first, without transcripts.
| Query parameter | Description |
|---|---|
status | in_progress, completed, not_connected |
direction, channel | inbound or outbound; phone or whatsapp |
profile, outcome | A profile ID; an outcome code, for example PROMISE_TO_PAY |
contactId, campaignId | Only calls for this contact or campaign |
from, to | Time range (timestamps) |
limit, cursor | Paging, see basics |
/v1/calls/{callId}/cancelXURA GLOBALStop a call. A ringing call is cancelled (call.not_connected, CANCELED). A live call is ended politely and analysed as usual (call.completed, end reason ended_by_operator).
Campaigns
/v1/campaignsXURA GLOBALCreate a campaign that calls a list of contacts automatically. Each contact is fetched from your Contact API just before its call.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A name your staff recognise (up to 100 characters). |
profile | string | If several | The call profile. Required when you have more than one outbound profile. |
channel | string | No | phone (default) or whatsapp. |
contactIds | string[] | Yes | 1 to 1,000 of your contact IDs, called in this order. |
maxAttempts | integer | No | Calls per contact if unanswered, busy or failed: 1–5, default 2. |
retryMinutes | integer | No | Wait before the next attempt: 1–1440, default 60. |
callingHours | object | No | start and end ("HH:MM", local time), days (MON…SUN) and skipDates (dates with no calls, such as public holidays). Default: your default calling hours. |
start | boolean | No | true starts calling right away. Default false (created as a draft). |
{
"name": "October appointment reminders",
"profile": "appointment-reminder",
"contactIds": ["P-2291", "P-2307", "P-2315"],
"maxAttempts": 3,
"retryMinutes": 90,
"callingHours": {
"start": "09:00",
"end": "18:00",
"days": ["MON", "TUE", "WED", "THU", "FRI", "SAT"],
"skipDates": ["2026-10-20"]
},
"start": true
}
{
"id": "CAMP-6D2E91AF",
"name": "October appointment reminders",
"profile": "appointment-reminder",
"channel": "phone",
"status": "running",
"totals": { "contacts": 3, "done": 0, "pending": 3, "calling": 0, "skipped": 0 },
"createdAt": "2026-09-25T03:29:47Z"
}
/v1/campaigns/{campaignId}XURA GLOBALCampaign progress with one row per contact.
{
"id": "CAMP-6D2E91AF",
"status": "running",
"totals": { "contacts": 3, "done": 1, "pending": 1, "calling": 1, "skipped": 0 },
"byOutcome": { "RESCHEDULE_REQUESTED": 1 },
"items": [
{ "contactId": "P-2291", "status": "done", "attempts": 1, "lastOutcome": "RESCHEDULE_REQUESTED", "lastCallId": "8d4a0f6e-…" },
{ "contactId": "P-2307", "status": "pending", "attempts": 1, "lastOutcome": "NO_ANSWER", "nextAttemptAt": "2026-09-25T06:32:41Z" },
{ "contactId": "P-2315", "status": "calling", "attempts": 1, "lastOutcome": null }
]
}
/v1/campaigns/{campaignId}/start | /pause | /stopXURA GLOBAL
Start a draft or paused campaign. Pause it (a call already in progress finishes). Stop it for good: contacts not yet
called are skipped and campaign.completed is sent with status stopped.
Profiles
/v1/profilesXURA GLOBALList your call profiles with the context fields they need and the outcome codes and data they return. Useful for building forms and reports in your front end.
{
"data": [
{
"id": "payment-reminder",
"name": "Payment reminder",
"directions": ["outbound", "inbound"],
"contextFields": [
{ "name": "amountDue", "type": "number", "required": true },
{ "name": "currency", "type": "string", "required": true },
{ "name": "dueDate", "type": "date", "required": true }
],
"outcomes": ["PROMISE_TO_PAY", "ALREADY_PAID", "FINANCIAL_HARDSHIP", "REFUSED_TO_PAY", "DISPUTED"],
"captures": [
{ "name": "promisedDate", "type": "date" },
{ "name": "promisedAmount", "type": "number" },
{ "name": "paidDate", "type": "date" }
]
}
],
"nextCursor": null
}
Knowledge base
The agents answer questions about your products, services and procedures from these documents. Instruction documents tell the agents how to work (tone, limits, when to hand over to your staff) and are always used. Knowledge documents hold the facts they answer from. Changes apply from the next call.
/v1/knowledge/documentsXURA GLOBALAdd a document as text (Markdown) or as a file: .md, .txt, .pdf or .docx, up to 10 MB, sent in base64.
{
"title": "ශාඛා සහ විවෘත වේලාවන් (Branches and opening hours)",
"type": "knowledge",
"language": "si",
"content": "# සෙනසුරාදා\n- හැම ශාඛාවක්ම උදේ 9.00 සිට දවල් 1.00 දක්වා විවෘතයි…"
}
{
"type": "knowledge",
"fileName": "service-procedures.pdf",
"fileBase64": "JVBERi0xLjQKJ…"
}
{
"id": "DOC-4B7E19C2",
"title": "ශාඛා සහ විවෘත වේලාවන් (Branches and opening hours)",
"type": "knowledge",
"language": "si",
"enabled": true,
"characters": 1320,
"passages": 6,
"updatedAt": "2026-09-25T04:10:12Z"
}
Use short sections with headings, one fact per line and exact numbers. Write numbers the way they should be spoken, with digits in brackets: සියයට 2ක් (2%). Anything missing from the documents becomes a call-back request, so write down every answer you expect the agents to give.
/v1/knowledge/askXURA GLOBALPreview an answer. Useful after editing a document, and for a "test a question" box in your front end.
{ "question": "සෙනසුරාදා ශාඛා ඇරලා තියෙනවද?" }
{
"answer": "ඔව්. හැම ශාඛාවක්ම සෙනසුරාදා උදේ නවයේ ඉඳන් දවල් එක වෙනකම් ඇරලා තියෙනවා.",
"passages": [
{ "documentId": "DOC-4B7E19C2", "title": "ශාඛා සහ විවෘත වේලාවන්", "heading": "සෙනසුරාදා", "score": 0.84 }
]
}
Webhook and settings
/v1/webhookXURA GLOBALSet your webhook URL and, optionally, the events you want. POST /v1/webhook/test then sends a signed webhook.test event so you can check your endpoint.
{
"url": "https://api.your-company.com/webhooks/xura",
"events": ["call.completed", "call.not_connected", "campaign.completed"]
}
/v1/settingsXURA GLOBAL
Agent settings. voice is one of the female voices Kore, Aoede, Leda,
Zephyr or the male voices Charon, Orus, Puck, Fenrir.
defaultCallingHours has the same shape as a campaign's callingHours and applies to campaigns that do not set their
own. Until you set it, campaigns call 08:00–19:00, Monday to Saturday.
{
"voice": "Kore",
"defaultCallingHours": { "start": "08:00", "end": "19:00", "days": ["MON", "TUE", "WED", "THU", "FRI", "SAT"] }
}
/v1/eventsXURA GLOBAL · OPTIONAL
Live updates as a Server-Sent Events stream, for a front end that shows calls and transcripts while they happen. Open it from your
server and forward it to your staff's screens. Events: call.started, call.transcript (one new line of the
conversation), call.completed, call.not_connected.
event: call.transcript
data: {"callId":"3ec2bcda-…","entry":{"role":"contact","text":"ඔව්, නිමල් කතා කරන්නේ.","at":"2026-09-25T04:44:58Z"}}
Errors
Errors use standard HTTP status codes and this body:
{
"error": {
"code": "contact_not_found",
"message": "Contact C-99999 was not found in your Contact API.",
"requestId": "req_8f2c61d09a"
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or invalid; message says which. |
| 401 | unauthorized | Missing or wrong API key. |
| 404 | not_found, contact_not_found, profile_not_found | The call, campaign, document, contact or profile does not exist. |
| 409 | conflict | The action does not fit the current state, for example starting a finished campaign. |
| 413 | file_too_large | A document is larger than 10 MB. |
| 422 | contact_not_callable | The contact has doNotCall: true. |
| 422 | channel_not_enabled | The channel is not set up for your organization, for example WhatsApp. |
| 429 | rate_limited | Too many requests; wait for Retry-After seconds. |
| 502 | contact_api_error | Your Contact API did not answer correctly in time. |
| 503 | line_unavailable | The phone line is down. Nothing was dialled; try again later. |
Quote the requestId when you contact us about an error.
Your team, your telecom operator and Xura Global4Phone line (SIP trunk)
The agents make and receive ordinary phone calls over a SIP trunk: phone lines that a licensed telecom operator delivers over an IP network. Your organization orders the trunk and its numbers from the operator and keeps that contract and its call charges. Xura Global connects the trunk to the telephony gateway in your system.
Connection options
Xura Global gives your team test phone accounts (an app on a phone or computer) connected to the gateway. Every test except the phone line tests can start before the trunk is connected.
Requirements
| Item | Requirement |
|---|---|
| Signalling | SIP (RFC 3261) over UDP, TCP or TLS. TLS is recommended where your operator supports it. |
| Audio | RTP, or SRTP where supported. Codecs: G.711 A-law or µ-law (required), G.722 (optional). |
| Keypad tones | RFC 4733 (RFC 2833) telephone events. |
| Authentication | IP allowlisting of your system's gateway (preferred), or registration with a username and password. |
| Number format | E.164 preferred. National formats are converted by the gateway. |
| Caller ID | Outbound calls show a number your operator has assigned to your organization. |
| Channels | Enough simultaneous calls for your busiest hour, inbound and outbound together. |
| Network quality | About 100 kbit/s each way per call (G.711). Latency below 150 ms, jitter below 30 ms, packet loss below 1%. |
What each side provides
Your team provides
- Operator name and the chosen option (A, B or C)
- SIP server addresses, port and transport
- Authentication: IP allowlisting, or a username and password sent securely
- The numbers routed to the agents, and the caller ID for outbound calls
- Number of channels, codecs and dialling format
- The operator's signalling and audio IP ranges
- The destinations the agents may call, for example Sri Lankan numbers only
Xura Global provides
- Gateway address and SIP port
- Public IP addresses for your operator's or PBX's allowlist
- Audio (RTP) port range
- Test phone accounts
- For option C, the gateway specification and setup guide
Routing and handover
- Inbound. Every call to a number routed to the agents is answered by an agent, day and night. To send only some calls, route them in your PBX (option B).
- Outbound. Calls show your caller ID. Campaign calls are placed only within your calling hours; calls you start one by one through the Voice API are placed straight away.
- Handover to staff. In version 1 the agents do not transfer live calls. When a contact needs a person, the agent promises a call back and the result arrives as
CALLBACK_REQUESTEDwith the reason.
Use numbers and trunks from a licensed telecom operator and follow your telecom regulator's rules (in Sri Lanka, the TRCSL). Xura Global does not connect SIM-based GSM gateways ("SIM boxes") for business calling: operators block them and they can breach telecom rules.
OptionalWhatsApp calling
Customers can also call your organization on WhatsApp, and the agents can call customers on WhatsApp once they allow it. WhatsApp is an extra
line, not a separate system: the calls reach the same agents, use the same Contact API and send the same webhook events, with
channel: "whatsapp".
How it connects
Meta connects WhatsApp calls for your business number to the gateway in your system over SIP, using the WhatsApp Business Calling API. Meta requires encrypted signalling (SIP over TLS) and encrypted audio (SRTP); the gateway handles both.
What you need
- A WhatsApp Business Platform (Cloud API) account for your organization. Numbers used in the WhatsApp Business app cannot be used.
- The WhatsApp number your customers will call, enabled for calling. Meta requires the number to have a messaging limit of at least 2,000 unique recipients a day.
- Access for Xura Global to set up calling on that number in your Meta business account, or your WhatsApp provider points the number's SIP calling to the address we give you. A number has one SIP destination for calls; your WhatsApp messages are not affected.
- For outbound calls: an approved message template that asks customers for permission to call.
Meta's rules for WhatsApp calls
| Rule | Detail |
|---|---|
| Customer calls you | Available wherever WhatsApp Business calling is offered. Free of charge. |
| You call a customer | Only after the customer gives permission in reply to a WhatsApp message. Not available in the USA, Canada, Egypt, Vietnam or Nigeria. |
| Permission | Temporary (7 days) or permanent, as the customer chooses. A customer who calls you gives temporary permission automatically. |
| Asking for permission | At most 1 request in 24 hours and 2 in 7 days for each customer. |
| Call limit | Up to 100 connected calls with each customer in 24 hours. |
| Unanswered calls | After 4 unanswered calls in a row, the permission is withdrawn. |
| Charges | Calls from customers are free. Calls you make are charged by Meta in 6-second steps, by destination country. Permission requests are charged as messages. |
Meta changes these rules from time to time. The current rules are in Meta's WhatsApp Business Calling documentation.
In the API
- Inbound. We look up the caller's WhatsApp number with your Contact API as usual. If WhatsApp does not share the caller's number, the caller is treated as unknown.
- Outbound.
POST /v1/callsorPOST /v1/campaignswithchannel: "whatsapp". A contact who has not given permission is reported ascall.not_connectedwithFAILEDand end reasonno_whatsapp_permission, and is not retried. - Permission requests are sent from your WhatsApp Business account with your approved template, by Xura Global on your behalf or by your own WhatsApp system.
Security and data protection
Credentials
- The Voice API key and the webhook signing secret (from Xura Global), the Contact API key (from you) and, if your trunk uses registration, the SIP password.
- Your Google Cloud keys are stored only on your own system.
- Keep them in a secrets store on your servers, never in front-end code, email or chat.
- Rotate them every 90 days and whenever someone with access leaves.
Network and phone line
- HTTPS with TLS 1.2 or later in both directions.
- You allow only your system's address on your Contact API and webhook; on request, your Voice API accepts requests only from your servers.
- The SIP trunk accepts traffic only from agreed addresses, and outbound calls go only to the destinations agreed at onboarding, which protects your trunk from toll fraud.
Personal data
- Your system is dedicated to your organization: data, keys and phone lines are never shared with other organizations.
- The Contact API returns only the fields in this guide, and context holds only what the call needs.
- Transcripts contain personal information: store them encrypted and limit who can read them.
- Version 1 keeps transcripts, not audio recordings.
Compliance
- Speech recognition and the AI model run in your organization's own Google Cloud project, so call data may be processed outside your country. This and the retention period are set out in the data processing agreement under the data protection law that applies to you (in Sri Lanka, the Personal Data Protection Act, No. 9 of 2022).
- The agents never call contacts marked
doNotCall, report opt-out requests, and campaigns call only within your calling hours.
Onboarding checklist
Both teams exchange the following before testing starts. Send secrets through a password manager or an encrypted channel, never by plain email.
Your team sends Xura Global
- Completed call profile sheet for each profile
- ① Contact API base URL and API key, for testing and for production
- ② Webhook URL, for testing and for production
- Test contacts: IDs and your staff's phone numbers
- ④ SIP trunk details (see what each side provides)
- Google Cloud project with billing enabled, and access for Xura Global to create the keys
- Server: a VPS in your name with administrator (SSH) access for Xura Global, or a request for hosting by Xura Global
- Your server addresses, if we should restrict the Voice API to them
- Knowledge documents
- Optional: WhatsApp Business account access and number
- A technical contact and an escalation contact
Xura Global sends your team
- ③ Your system's address (Voice API base URL) and API keys (
xv_test_…,xv_live_…) - ② Webhook signing secret for each environment (
whsec_…) - Your system's IP address, to allow on your firewall
- ④ Gateway address, ports and public IP addresses
- Test phone accounts, so your team can play the contact
- A technical contact and an escalation contact
| Testing | Production | |
|---|---|---|
| Voice API address | The same for both: your system's base URL | |
| Voice API key | xv_test_… | xv_live_… |
| Phone line | Test phone accounts, then your trunk with your staff's phones as test contacts | Your trunk and numbers |
| Contact data | Your test Contact API with test contacts | Your production Contact API |
| Webhook | Your test endpoint | Your production endpoint |
Testing and go-live
Acceptance tests
We run these together in the testing environment. Each must pass before go-live; test 15 applies only if you use WhatsApp.
| # | Test | Expected result |
|---|---|---|
| Data and results | ||
| 1 | ① Look up a test contact's phone number | 200 with the contact object, in under 800 ms |
| 2 | ① Look up an unknown number; call with a wrong key | 404; 401 |
| 3 | ② POST /v1/webhook/test | Your endpoint verifies the signature and answers 200 |
| 4 | ② The same test event with a changed body | Your endpoint answers 401 |
| 5 | ② The same event sent twice | Stored and processed once |
| Calls | ||
| 6 | ③ Call a test contact and complete the profile's main goal | call.started, then call.completed with the expected outcome code and data |
| 7 | ③ Call a test contact and do not answer | call.not_connected with NO_ANSWER after about 30 seconds |
| 8 | ④ Call your number from a test contact's phone | The agent answers and addresses the contact by name after confirming it |
| 9 | ④ Outbound call through the trunk | The phone shows your caller ID; clear audio both ways |
| 10 | ④ As many calls at once as your trunk has channels | All calls connect with clear audio |
| Conversation | ||
| 11 | Ask a question from the knowledge base, then one that is not in it | A correct answer; then a call-back promise (CALLBACK_REQUESTED) |
| 12 | Say "please don't call me again" | The call ends politely with OPT_OUT |
| Campaigns and rules | ||
| 13 | ③ Campaign of three test contacts with one retry | One event per attempt, then campaign.completed |
| 14 | A test contact with doNotCall: true | Not called: 422 for a single call, CANCELED in a campaign |
| Optional | ||
| 15 | WhatsApp: call your WhatsApp number; then an outbound WhatsApp call after permission | The agent answers; the outbound call connects |
Go-live checklist
- All acceptance tests passed and signed off by both teams
- Call profiles and knowledge documents approved by your business and compliance teams
- Production keys and the signing secret exchanged and stored in secrets stores
- Firewalls allow your system's address; production webhook URL set
- Google Cloud billing enabled and a monthly budget alert set
- Production trunk: numbers routed to the agents, caller ID and allowed destinations checked
- Calling hours, holidays and do-not-call flags checked; opt-outs update your CRM
- Pilot with a small group of contacts, reviewed before the full rollout
- Monitoring agreed: failed webhooks, Contact API errors and phone line faults alert both teams
Reference codes
Standard outcome codes
Used by every call profile.
| Code | Meaning | Suggested follow-up on your side |
|---|---|---|
CALLBACK_REQUESTED | The contact asked for a person to call back, or asked something the agent could not answer | Your staff call back; add missing answers to the knowledge base |
OPT_OUT | The contact asked not to be called again | Set doNotCall in your CRM |
WRONG_PERSON | Someone else answered, or the number does not belong to the contact | Check the contact details |
NO_RESPONSE | The call connected but nobody spoke | Try again later |
INCOMPLETE | The call ended before a result was reached | Try again later |
NO_ANSWER | Not connected: nobody answered within 30 seconds | Retried automatically in campaigns |
BUSY | Not connected: busy or declined | Retried automatically in campaigns |
FAILED | Not connected: the number could not be reached, contact data was not available, or there was no WhatsApp permission | Check the number, the Contact API or the permission |
CANCELED | Not connected: cancelled by your system, or the contact is marked do-not-call | — |
Profile outcome codes (examples)
| Code | Meaning | Captured data |
|---|---|---|
| Payment reminder | ||
PROMISE_TO_PAY | Agreed to pay an amount on a date, and confirmed it | promisedDate, promisedAmount |
ALREADY_PAID | Says the amount has already been paid | paidDate, if given |
FINANCIAL_HARDSHIP | Cannot pay now (job loss, illness, business losses) | — |
REFUSED_TO_PAY | Refused to pay | — |
DISPUTED | Disputes the amount or the debt | — |
| Appointment reminder | ||
CONFIRMED | Will attend as booked | — |
RESCHEDULE_REQUESTED | Wants another date or time | requestedDate |
CANCELLED | Will not attend | — |
| Order and delivery confirmation | ||
CONFIRMED | Confirms the order and the delivery | — |
CHANGE_REQUESTED | Wants to change the address, date or items | changeNote |
CANCELLED | Cancels the order | — |
| Lead follow-up | ||
INTERESTED | Wants to hear more or to meet | productOfInterest, preferredTime |
NOT_INTERESTED | Is not interested | — |
| Customer service line | ||
RESOLVED | The question was answered from the knowledge base | topic |
| Feedback survey | ||
SURVEY_COMPLETED | Answered the survey | score, comments |
DECLINED | Did not want to take part | — |
End reasons
| Code | Meaning |
|---|---|
agent_completed | The agent finished the conversation and said goodbye |
contact_hangup | The contact hung up |
ended_by_operator | Ended through POST /v1/calls/{callId}/cancel |
max_duration | Reached the 10-minute limit |
technical_error | A technical problem with the phone audio or the AI service ended the call; the outcome is usually INCOMPLETE |
no_answer, busy, failed, canceled | Not connected (see the outcome code) |
do_not_call, contact_not_found, contact_api_error, no_whatsapp_permission | Not placed, for the reason named |
Call status
queued → dialing → in_progress → completed. A call that is not answered goes from
dialing to not_connected. Inbound calls start at in_progress.
Campaign status
draft → running ⇄ paused → completed or stopped. A campaign completes when
every contact has a final result.
Each contact in a campaign is pending (waiting for its first call or a retry), calling, done (has a final
result) or skipped (not called: do-not-call, not found in your Contact API, or no WhatsApp permission).
Document history
| Version | Date | Changes |
|---|---|---|
| 1.0 | 25 September 2026 | First release |