Developers

Build translated calls into your own systems

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.

On this page

Authentication

Authenticate with an API key

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: GET /billing
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

Integration endpoints

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

Place a translated call

Send the agent's number and the far party's number. Languages and caller IDs are optional, and unknown fields are rejected.

POST /calls/originate
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"
  }'
201 Created
{
  "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: GET /calls
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: GET /twilio-numbers
curl https://b2b.owaa.ai/v1/b2b/admin/twilio-numbers \
  -H "Authorization: Bearer $API_KEY"
200 OK
[
  {
    "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: PUT /twilio-numbers/{id}/route
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

Route inbound calls with a signed webhook

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.

Request body

call.inbound
{
  "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.

response: connect
{ "action": "connect", "destination": "+34600000000", "caller_id": "+14165550101" }
response: reject
{ "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.

Node.js
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);
}
Python
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

Rate limits and error responses

Limits apply per business, each minute. When one is exceeded the API returns 429 with a Retry-After header.

  • 600 read requests per minute
  • 120 write requests per minute
  • 60 recording downloads per minute
  • 401Missing or invalid API key.
  • 402Insufficient balance to start the call.
  • 409No usable caller ID, or no verified Twilio account connected.
  • 422Twilio refused the call, or a caller ID you named is not one you own.
  • 429Rate limit exceeded: wait for the time in Retry-After. Also returned to an address that is temporarily locked out after repeated invalid keys.

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

Let AI assistants work with your account

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.

Available on request

The MCP server is available on request. Contact us to enable it for your business.

Endpoint

https://mcp.owaa.ai/business

Streamable HTTP transport.

Connect

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.

Client configuration

Add the server to any MCP client that supports remote HTTP servers, and replace the placeholder with your key.

mcp.json
{
  "mcpServers": {
    "owaa": {
      "type": "http",
      "url": "https://mcp.owaa.ai/business",
      "headers": { "Authorization": "Bearer <your-api-key>" }
    }
  }
}
Claude Code
claude mcp add --transport http owaa https://mcp.owaa.ai/business \
  --header "Authorization: Bearer <your-api-key>"

Tools

  • list_languagesList the languages available for translated calls.
  • check_call_costEstimate what a call will cost before it is placed.
  • get_callGet one call, with its status and cost.
  • list_recent_activityList recent activity on your account.
  • place_translated_callPlace a translated call: rings your operator's phone, then the far party. Spends your balance.
  • delegate_call_taskHand a phone task to Owaa's AI caller, with a goal and the fields to collect. Available when enabled for your business.
  • get_taskCheck the progress and result of a delegated task.
  • cancel_taskCancel a delegated task.

Scopes

  • activity:readLook up languages, costs, calls and tasks. Always granted.
  • calls:translatePlace translated calls.
  • tasks:delegateDelegate phone tasks.

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

How access is protected

Encrypted in transit

All API and MCP traffic uses TLS, with HSTS.

API keys

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.

Rate limits

600 reads, 120 writes and 60 recording downloads per minute per business, with 429 and Retry-After when exceeded.

Signed webhooks

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.

Your carrier account

Calls run on your own Twilio account, and your Twilio credentials are encrypted at rest.

MCP authorization

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.

Separate scopes

Reading and spending are separate scopes. An assistant needs calls:translate or tasks:delegate before it can place a call or start a task.

Spend limits an assistant cannot raise

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.

Allow-lists

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.

Audit trail

Every MCP tool call is recorded with its outcome and the client that made it.

Transcripts are opt-in

Call transcripts are off by default and enabled per business.

Get access

Talk to us about API and MCP 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.

Open the console
Owaa developer docs — merchant API and MCP server