Encrypted in transit
All API and MCP traffic uses TLS, with HSTS.
Developers
Owaa for Business offers a REST API for placing and reviewing translated calls, a signed webhook for inbound routing, and an MCP server that lets AI assistants work with your account under limits you control.
Authentication
Every request carries your key as a Bearer token. Create keys in the Owaa for Business console. A key is shown once when it is created, so store it in your secrets manager.
Base URL
https://b2b.owaa.ai/v1/b2b/admin
curl https://b2b.owaa.ai/v1/b2b/admin/billing \
-H "Authorization: Bearer $API_KEY"Send the key in the Authorization header. Keys are not accepted as a query parameter.
API keys can only call the integration endpoints listed on this page, and the MCP server. Managing API keys, team members, billing top-ups, carrier credentials and spend limits requires a signed-in console session.
Rotate a key from the console (API keys page): the new key is shown once and the old one stops working immediately.
A request with a missing or invalid key returns 401. Repeated failed attempts from one address are locked out for a while.
REST API
All paths are relative to the base URL. Requests and responses are JSON, except recordings, which stream audio.
POST/calls/originate
Start a translated call between your agent's phone and a far party, using your own connected Twilio account.
GET/caller-id/preview?destination=
Dry-run a destination: shows which caller ID would be used, so caller-ID problems surface before you dial.
GET/calls
List call legs, newest first. Filter with limit (1 to 200) and status, and page with the cursor in next_cursor.
GET/calls/{id}
Fetch a single call leg.
GET/calls/{id}/recording
Stream a leg's recording as Opus audio. Choose the side with side=in or side=out.
GET/billing
Read your balance, per-minute rate, minutes remaining and low-balance threshold.
GET/twilio-numbers
List your Twilio numbers with their id, routing settings and status. Read-only: no credentials are returned.
PUT/twilio-numbers/{id}/route
Set the inbound routing URL and the fallback destination for one of your Twilio numbers.
Example
Send the agent's number and the far party's number. Languages and caller IDs are optional, and unknown fields are rejected.
curl -X POST https://b2b.owaa.ai/v1/b2b/admin/calls/originate \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_number": "+14165550100",
"far_number": "+34600000000",
"agent_language": "en",
"far_language": "es"
}'{
"bridge_id": "...",
"agent_call_sid": "...",
"far_call_sid": "...",
"agent_caller_id": "+14165550101",
"far_caller_id": "+14165550101",
"agent_must_confirm": false
}A successful request returns 201 with the identifiers of the call. Run the caller-ID preview first to avoid the 409 cases.
List recent call legs. Use the billing endpoint in the same way to read your balance.
curl "https://b2b.owaa.ai/v1/b2b/admin/calls?limit=20" \
-H "Authorization: Bearer $API_KEY"To set routing you need the id of the number. Find it with GET /twilio-numbers, which lists your numbers (shown here with the main fields).
curl https://b2b.owaa.ai/v1/b2b/admin/twilio-numbers \
-H "Authorization: Bearer $API_KEY"[
{
"id": "<number_id>",
"phone_number_e164": "+14165550101",
"friendly_name": "Support line",
"country_code": "CA",
"voice_enabled": true,
"verified": true,
"routes_to_owaa": true,
"inbound_destination_e164": "+34600000000",
"inbound_routing_url": "https://example.com/owaa/route",
"agent_language": "es",
"caller_language_hint": "en"
}
]Then set inbound routing for that number with PUT /twilio-numbers/{id}/route: the URL Owaa calls for each inbound call, the fallback destination, and optionally the agent language and a language hint for the caller.
curl -X PUT https://b2b.owaa.ai/v1/b2b/admin/twilio-numbers/<number_id>/route \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"routing_url": "https://example.com/owaa/route",
"destination_e164": "+34600000000",
"agent_language": "es",
"caller_language_hint": "en"
}'Inbound routing
For each inbound call on a number routed to your URL, Owaa sends a POST and waits up to three seconds. If your endpoint fails, times out or returns invalid JSON, the call goes to the fallback destination you configured and the reason is recorded.
{
"event": "call.inbound",
"call_id": "...",
"tenant_id": "...",
"from": "+14165550100",
"to": "+14165550101",
"twilio_call_sid": "...",
"received_at": "..."
}Reply with 200 and connect or reject. A caller_id that you do not own is ignored.
{ "action": "connect", "destination": "+34600000000", "caller_id": "+14165550101" }{ "action": "reject", "reason": "closed" }X-Owaa-Signature Each request carries an X-Owaa-Signature header in the form t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with your routing secret. Verify it against the raw body and reject timestamps older than five minutes.
Your routing secret is shown once on the Routing page of the console.
The timestamp window limits replays to five minutes. To block replays inside it, also remember the call_id of each request you have already handled.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the exact bytes received, as a string or Buffer.
// Never re-serialise parsed JSON: that changes the bytes and the signature.
export function verifyOwaaSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header ?? "").split(",").map((p) => p.split("=")),
);
const t = parts.t ?? "";
if (!/^\d+$/.test(t) || !(Math.abs(Date.now() / 1000 - Number(t)) <= 300)) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
def verify_owaa_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > 300:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Limits and errors
Limits apply per business, each minute. When one is exceeded the API returns 429 with a Retry-After header.
The API has no idempotency key. If a request to place a call times out, list your recent calls before sending it again.
MCP server
The Owaa MCP server lets MCP-compatible AI assistants look up call costs, place translated calls and review activity for your business, inside the spending limits you set.
The MCP server is available on request. Contact us to enable it for your business.
Endpoint
https://mcp.owaa.ai/business
Streamable HTTP transport.
With an API key: send it as a Bearer token in the Authorization header.
With OAuth: clients that support OAuth 2.1 with PKCE can sign in and are granted access from the console. Each grant can be revoked.
Add the server to any MCP client that supports remote HTTP servers, and replace the placeholder with your key.
{
"mcpServers": {
"owaa": {
"type": "http",
"url": "https://mcp.owaa.ai/business",
"headers": { "Authorization": "Bearer <your-api-key>" }
}
}
}claude mcp add --transport http owaa https://mcp.owaa.ai/business \
--header "Authorization: Bearer <your-api-key>"Scopes for an API key are set in the console. A tool outside the key's scopes returns scope_not_granted.
The server instructs assistants to check the cost and confirm with the user before any call that spends money.
Tools return errors as a code and a message, for example insufficient_balance, country_not_allowed or rate_limited. Do not retry outcome_unknown blindly: check the call first.
Security
All API and MCP traffic uses TLS, with HSTS.
Keys are long random secrets, shown once when created and stored only as a hash. Revoke or rotate a key at any time and the old one stops working immediately. Repeated failed attempts are locked out.
600 reads, 120 writes and 60 recording downloads per minute per business, with 429 and Retry-After when exceeded.
Inbound routing requests are signed with HMAC-SHA256 and a timestamp. Verify the signature and reject requests older than five minutes. A reply cannot present a caller ID your business does not own.
Calls run on your own Twilio account, and your Twilio credentials are encrypted at rest.
OAuth 2.1 with PKCE. Tokens are bound to the MCP resource, access tokens are short-lived, refresh tokens rotate on every use, and a replayed refresh token revokes the grant. Only digests of tokens are stored, and grants can be revoked from the console.
Reading and spending are separate scopes. An assistant needs calls:translate or tasks:delegate before it can place a call or start a task.
Each key has a daily spend limit and your business has a daily ceiling, both with conservative defaults and a platform maximum. Only a signed-in console session can change them, never an API key or an AI client.
Calls are limited to a country allow-list, and you can restrict which operator phone numbers an assistant may ring. Both are managed outside the MCP connection.
Every MCP tool call is recorded with its outcome and the client that made it.
Call transcripts are off by default and enabled per business.
Get access
Create API keys in the console once your business is set up. To enable the MCP server, or to discuss an integration, contact us.