zoxaAI
Homepage
API ReferenceTelephony Configs & Numbers

Tata Smartflo setup

Agent routing on Tata Smartflo — streaming-native specifics, API-first inbound bindings with server-resolved credentials, and saved-agent attachments.

Tata Smartflo is streaming-native: instead of POSTing to a per-call HTTP webhook (like Twilio/Vobiz), it opens a media stream to a single, static, signed WSS URL. Two consequences:

  1. Inbound routing needs a one-time portal step (below) — there's no per-number webhook for zoxaAI to provision.
  2. POST /call requests for Smartflo carry no inline credentials — zoxaAI resolves them server-side from the saved telephony configuration that owns the number.

Prerequisites

  1. A Smartflo telephony configuration with an auth_token.
  2. The number added and active under that config (add a phone number).
  3. The config's Voice Bot WSS URL pasted into the Tata portal (one-time, below).

One-time: point Tata at zoxaAI

Fetch voice_bot_wss_url from GET /telephony-configs/{config_id} and paste it into Settings → Channels → Voice Bot in the Tata Smartflo portal.

Signed per config

The URL is signed with your config_id, so every inbound stream on this Smartflo account arrives at zoxaAI. There's no per-number webhook to register — routing to the right agent happens after the stream connects, from the dialed DID.

Inbound via the API (transient agents)

Register the DID with POST /call type=inbound — same request shape as Twilio/Vobiz, except callConfig has no auth (sending one returns a 400 with Extra inputs are not permitted):

curl -X POST https://dashboard.zoxa.ai/api/v1/call \
  -H "X-API-Key: zsk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "inbound",
    "callConfig": {
      "provider": "smartflo",
      "phoneNumber": "+919000000000"
    },
    "agent": {
      "name": "Reception bot",
      "systemPrompt": "You are the front desk. Greet callers and route them.",
      "greeting": { "firstMessages": ["Thanks for calling! How can I help?"] },
      "llm": { "provider": "openai", "model": "gpt-5.4-mini" },
      "tts": { "provider": "elevenlabs" },
      "stt": { "provider": "soniox" }
    }
  }'
import httpx

httpx.post(
    "https://dashboard.zoxa.ai/api/v1/call",
    headers={"X-API-Key": "zsk_..."},
    json={
        "type": "inbound",
        "callConfig": {
            "provider": "smartflo",
            "phoneNumber": "+919000000000",
        },
        "agent": {
            "name": "Reception bot",
            "systemPrompt": "You are the front desk. Greet callers and route them.",
            "greeting": {"firstMessages": ["Thanks for calling! How can I help?"]},
            "llm": {"provider": "openai", "model": "gpt-5.4-mini"},
            "tts": {"provider": "elevenlabs"},
            "stt": {"provider": "soniox"},
        },
    },
)

All three agent modes work — inline agent (transient), agentId (persistent), or agentId + overrides.

How credentials are resolved

zoxaAI looks up the requested phoneNumber among your org's active Smartflo phone numbers (format-tolerant: +919000000000, 919000000000, and 9000000000 all match the same stored DID), loads the owning configuration's credentials, then runs a live token check and a number-ownership probe against Tata before registering — the DID must actually belong to the Tata account, same guarantee as Twilio's "not owned by this account" check. The binding stores:

  • the number's canonical E.164 (address_normalized), and
  • a credentials snapshot from the config — rotated your token? Re-POST the binding to re-snapshot.

Registration errors

StatusdetailWhen
400"phone_number_not_configured: add this number to a Smartflo telephony configuration first"The number isn't added (or isn't active) under any of your org's Smartflo configs.
400"smartflo_config_missing_token: ..."The owning config has no auth token.
400"invalid_credentials: <reason>"The saved token failed the live Tata probe (rotated/expired).
400"Phone number ... is not owned by this Smartflo account."The DID isn't among the numbers your Tata account exposes — you can only bind numbers your account owns.
400{"error": "Agent configuration validation failed", "details": [{"path": "callConfig.smartflo.auth", "message": "Extra inputs are not permitted", ...}]}You sent auth for smartflo, or other schema violations (POST /call formats schema errors as 400).
501not-implemented messagetype=outbound with smartflo (see below).

Managing the binding

Standard inbound-bindings lifecycle: GET to list, PATCH to swap the agent (no re-provisioning), DELETE to remove. Deleting the binding falls routing back to the number's dashboard agent attachment (if any).

Inbound via a saved agent (dashboard-style)

Alternatively, attach a saved agent to the phone number with its inbound_agent_uuid:

curl -X PUT https://dashboard.zoxa.ai/api/v1/organizations/telephony-configs/12/phone-numbers/88 \
  -H "X-API-Key: zsk_..." -H "Content-Type: application/json" \
  -d '{ "inbound_agent_uuid": "550e8400-e29b-41d4-a716-446655440000" }'

Editing the agent later (PATCH /agents/{agent_uuid}) needs no re-bind — the config is resolved fresh on each inbound call.

How an inbound stream resolves to an agent

When Tata streams in, zoxaAI checks, in order:

  1. A signed identifier in the stream params → a call we originated (outbound).
  2. An inbound binding for the dialed DID (format-tolerant match) → runs binding.agent_snapshot. Wallet and concurrency limits are enforced before the pipeline starts.
  3. The phone number with an inbound_agent_uuid → runs that saved agent.

A binding (step 2) wins over a saved-agent attachment (step 3) on the same number — re-registering via the API takes routing back.

Outbound

Place Smartflo outbound calls with POST /api/v1/call and a callConfig of { "provider": "smartflo", "phoneNumber": "+91..." } — no auth object. The dial routes through the saved Smartflo configuration that owns phoneNumber, so the number must be added and active under that configuration (otherwise 400 phone_number_not_configured). The configuration also needs a click_to_call_api_key.

Because Smartflo's originate API is async and returns no call id, zoxaAI attaches a signed custom_identifier to each dial so the returning media stream correlates back to the right call.

On this page