# Buy a number over the API (/docs/buy-a-number-api) > Search the OmniDimension number shop, buy a number, and release it when you are done. Everything the [OmniDimension number shop](/docs/dashboard-guides/numbers-shop) does in the dashboard, you can do from your own code: search what is available in a region, buy a number, list what you hold, and release one you no longer need. ## Search what is available [#search-what-is-available] ```http GET /api/v1/phone_number/search?region=IN ``` Narrow the results with `pattern`, and page through with `page` and `limit`. ```json { "success": true, "region": "IN", "numbers": [ { "phone_number": "+911155590077", "monthly_rental_usd": 5.0, "validity_days": 30, "region": "IN", "kyc_required": true } ], "total": 42, "page": 1, "limit": 20, "total_pages": 3 } ``` Price and validity are flat per region, so every result carries the same `monthly_rental_usd`, and that is the exact amount the purchase will charge. Searching does not reserve anything. A number is claimed only when a purchase starts, so a number you saw a moment ago can be gone by the time you buy it. ## Buy one [#buy-one] ```http POST /api/v1/phone_number/purchase Idempotency-Key: { "region": "IN", "phone_number": "+911155590077" } ``` The rental comes out of your wallet and the number is added to your account, ready to attach to an agent. It is how you recover a lost response. If your request times out, retry with the **same** key and you get your original order back with `replayed: true`, instead of guessing whether the first attempt went through. Use a fresh key for each new purchase: reusing an old one returns that old order, it is not checked against your new request. ### When a purchase is refused [#when-a-purchase-is-refused] Every refusal that can happen without spending money happens before anything is charged. | Status | Meaning | | -------------------------- | ---------------------------------------------------------------- | | `403 feature_disabled` | Phone number access is switched off for that account. | | `409 kyc_incomplete` | The region needs identity verification and it is not finished. | | `409 number_unavailable` | Someone claimed the number first. Search again and pick another. | | `409 in_progress` | A purchase for this number, or under this key, is still running. | | `402 insufficient_balance` | The wallet cannot cover the rental. Nothing was charged. | ## Identity verification [#identity-verification] Some regions require the account to be verified before it can buy. India is one: the check is an Aadhaar eKYC, so it completes in minutes. Verification done in the dashboard counts, it is the same gate either way. ## List and release [#list-and-release] ```http GET /api/v1/phone_number/list POST /api/v1/phone_number/release { "phone_number": "+911155590077" } ``` Releasing gives the number up and stops its rental, so it is not charged at the next renewal. ## Reference [#reference] Full request and response schemas, with a playground, are in the [API reference](/docs/api-reference/phone-numbers/searchPhoneNumbers). # Get a phone number (/docs/get-a-number) > Four ways to get a phone number for your AI agents. Buy one from OmniDimension, or import your own from Twilio, a SIP carrier, or Exotel. Every AI agent that makes or receives calls needs a phone number. You have four ways to get one. If you don't already have a number with another provider, buying one from OmniDimension is the fastest path. If you already own numbers, import them. Buying a number takes a couple of minutes and needs no external account. Only import if you already have numbers you want to keep using. ## Your options [#your-options] | Option | Best for | Setup | | ----------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | | [**Buy from OmniDimension**](/docs/dashboard-guides/numbers-shop) | Getting started fast, no existing provider | Pick a number, pay from wallet, done | | [**Import from Twilio**](/docs/telephony/twilio-import) | You already have Twilio numbers | Paste your Twilio SID and token | | [**Import via SIP**](/docs/telephony/sip) | Any other carrier (RingCentral, Vonage, and more) | Paste SIP trunk credentials | | [**Import from Exotel**](/docs/telephony/exotel-import) | You're already on Exotel | Build a call flow, then import | ## Buy from OmniDimension (recommended) [#buy-from-omnidimension-recommended] The quickest way to get a number. Browse available numbers in the dashboard, pay from your wallet, and attach the number to an agent. You can buy +91 (India) and +1 (US) numbers, and your number can make calls to 70+ countries. [Go to Numbers Shop →](/docs/dashboard-guides/numbers-shop) ## Import from Twilio [#import-from-twilio] If you already have Twilio numbers, import one directly with your Twilio account SID and auth token. No SIP trunk setup required. [Import a Twilio number →](/docs/telephony/twilio-import) ## Import via SIP [#import-via-sip] OmniDimension speaks SIP, the standard protocol every modern carrier supports. If your carrier can hand out SIP trunk credentials, you can plug it in. We have quick-start guides for RingCentral, Vonage, and Twilio's SIP trunking. [Set up SIP →](/docs/telephony/sip) ## Import from Exotel [#import-from-exotel] If you're on Exotel, you can import an Exotel number after building a call flow in your Exotel dashboard. [Import an Exotel number →](/docs/telephony/exotel-import) ## After you have a number [#after-you-have-a-number] Attach it to an agent from the **Phone Numbers** page in your dashboard, then make a test call. See [Configure your agent](/docs/dashboard-guides/configure-your-agent) to set up how it handles calls. # Documentation (/docs) > Build AI voice agents with OmniDimension. Guides, API reference, and integration recipes for the entire platform. [ Quickstart First request in five minutes. ](/docs/get-started/quickstart) [ API reference Interactive playground for every endpoint. ](/docs/api-reference) [ Examples Real-world builds to learn from and adapt. ](/docs/examples) ## Build [#build] " title="Configure your agent" href="/docs/dashboard-guides/configure-your-agent" description="Sections, instructions, welcome message, and conversational flow patterns." /> " title="Voices and languages" href="/docs/dashboard-guides/voices-and-languages" description="1000+ voices, 90+ languages, persona categories, and multilingual support." /> " title="Numbers shop" href="/docs/dashboard-guides/numbers-shop" description="Buy a number that calls 70+ countries and assign it to your agents." /> " title="Knowledge base" href="/docs/dashboard-guides/knowledge-base" description="Ground your agent's responses in your own documents and policies." /> ## Connect [#connect] " title="Bulk calls" href="/docs/bulk-calls" description="Outbound campaigns from CSV or via a real-time dynamic queue." /> " title="Telephony" href="/docs/telephony" description="SIP trunking with Twilio, RingCentral, Vonage, plus Exotel imports." /> " title="WhatsApp" href="/docs/whatsapp" description="Phone WhatsApp via QR or WhatsApp Cloud API via Meta credentials." /> " title="Integrations" href="/docs/integrations" description="HubSpot, Salesforce, Slack, Cal.com, Google Calendar, Zapier, custom APIs." /> ## Reference [#reference] " title="API reference" href="/docs/api-reference" description="Endpoint reference for agents, calls, numbers, knowledge base, and integrations." /> " title="Tutorials" href="/docs/tutorials" description="Video walkthroughs for the most common build steps." /> " title="Examples" href="/docs/examples" description="End-to-end use cases across healthcare, real estate, recruitment, and more." /> # Omnichannel deployment (/docs/omnichannel) > Deploy one agent across voice, web chat, WhatsApp, email, and SMS. You build an agent once, then deploy that same agent across multiple channels. The agent's prompt, knowledge base, voice, and integrations are shared, so behavior stays consistent no matter how a customer reaches it. ## Channels you can deploy to [#channels-you-can-deploy-to] * **Voice**: attach the agent to a phone number for inbound and outbound calls. See [Get a phone number](/docs/get-a-number). * **Web chat and voice widget**: embed the agent on your website. See [Web chat widget](/docs/dashboard-guides/web-chat-widget). * **WhatsApp**: connect a WhatsApp number. See [WhatsApp integration](/docs/whatsapp). * **Email**: handle email conversations with the agent. * **SMS**: handle SMS conversations with the agent. WhatsApp and voice are available by default. Email and SMS channels are enabled per account, so they may not appear in your dashboard until turned on. Contact support if you need them. ## How it works [#how-it-works] Each channel points back to the agent that is active on that surface, so the same configuration drives every channel. You can layer channel-specific tuning on top of the shared agent (for example, guidance that only applies to SMS) without duplicating the whole agent. Conversation history is kept per channel, so each channel has its own thread of messages. # Plans and pricing (/docs/pricing) > How OmniDimension voice AI plans work, what each plan includes, and how usage is billed. OmniDimension bills voice AI usage per minute, with a monthly plan that already includes a block of minutes at a set per-minute rate. Higher tiers lower your per-minute rate and unlock more of the platform. For the latest prices, always check the live pricing page at [omnidim.io/pricing](https://omnidim.io/pricing). ## How billing works [#how-billing-works] * Each plan's monthly price already covers its included minutes at the listed per-minute rate. In other words, monthly price divided by the per-minute rate is roughly the number of included minutes. * When you go past the included minutes, extra usage is charged at the plan's extra-usage rate. * Phone number rental and telephony are billed separately from your plan (see [Numbers Shop](/docs/dashboard-guides/numbers-shop)). * WhatsApp messages are billed per outgoing message from your wallet (see [WhatsApp pricing](/docs/whatsapp#pricing)). * Call concurrency (channels) is billed separately too, from your wallet balance (see [Concurrency and channels](#concurrency-and-channels)). * You can [top up credits](https://omnidim.io/billing) with a custom amount at any time from your dashboard. ## Voice AI plans [#voice-ai-plans] | Plan | Monthly | Per-minute rate | Included minutes | Extra usage | Knowledge base | Best for | | --------------- | ------------------ | ------------------------------- | ---------------- | ----------- | -------------- | ----------------------------- | | Starter | $15 | $0.084 | \~179 | +$0.009/min | 5 MB | Quick experiments and testing | | Jump Starter | $30 | $0.076 | \~395 | +$0.008/min | 10 MB | Building and sharing demos | | Early Deployers | $40 (10% off: $36) | $0.068 | \~588 | +$0.006/min | 50 MB | POCs with a live agent | | Growth | $200 | $0.056 | \~3,571 | +$0.005/min | 100 MB | Scaling usage after a POC | | Enterprise | Custom | Volume discounts down to $0.035 | Custom | Custom | Custom | Large-scale production | Enterprise adds a dedicated account manager, volume discounts down to $0.035/min, custom concurrency and unlimited seats, a custom knowledge base allowance, SSO with a custom DPA and security review, priority onboarding, and a dedicated SLA with under 4-hour P0 response. To discuss it, [contact sales](https://omnidim.io/contact-us?reason=general). ## What each plan includes [#what-each-plan-includes] Plans are cumulative: each tier includes everything in the tiers below it, plus its own additions. | Plan | Adds | | --------------- | ---------------------------------------------------------------------------------------------------------- | | Starter | Voice AI agents, chat and call logs, integrations | | Jump Starter | WhatsApp, voice cloning, SIP trunking | | Early Deployers | Agent versioning, live call monitoring, voicemail detection, workflow builder (10,000 executions/month) | | Growth | Recording insights, email channel, SMS channel, and a higher workflow allowance (100,000 executions/month) | Every plan also includes analytics, bulk calls, a knowledge base, phone number management, team management, and Google Calendar and Google Sheets actions. Accounts created before feature tiers were introduced keep the features they already had. ## Flexible model selection [#flexible-model-selection] Every plan supports any combination of supported models. You are not locked into a "basic" or "premium" tier: pick the STT, LLM, and TTS providers that fit each agent, and your usage is billed at your plan's per-minute rate. See [Voices and languages](/docs/dashboard-guides/voices-and-languages) for the available models. ## Concurrency and channels [#concurrency-and-channels] Concurrency is how many calls your agents can run at the same time. It applies to your whole organization, not to a single phone number, so every agent on every number draws from the same shared pool. * Your plan includes a base number of channels. * You can buy more channels from [Billing → Telephony → Add channels](https://omnidim.io/billing?tab=numbers). Each additional channel is $6.74 per month, paid from your wallet balance, and renews monthly. You can release a purchased channel at any time. * Total concurrency is your included channels plus purchased channels. For example, 10 included plus 5 purchased gives 15 concurrent calls. To size concurrency, use: channels = (calls per hour x average call minutes) / 60. Round up and add a buffer. ## Free trial [#free-trial] New accounts include about 10 minutes of free voice AI usage so you can try the platform before choosing a plan. ## White-label and agencies [#white-label-and-agencies] If you want to resell voice AI under your own brand, see [OmniRelay](/docs/omnirelay), which has its own agency plans and per-client billing. # Reseller API (/docs/reseller-api) > Run your clients from your own product: create them, set what they can do, fund them with credits, verify them, and buy them numbers. A reseller account owns client accounts. Each client is a separate organization with its own balance, its own agents and its own numbers, and you decide what each one may do and what it pays per minute. This API is that control panel, so your clients never have to see ours. Every call names one of your clients by `user_id` or `child_organization_id`, and only ever one of your own: any other id is rejected. Available to reseller accounts. [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1) if you do not have it yet. ## The shape of it [#the-shape-of-it] create the account fund credits, at your rate complete eKYC where required buy a number then it can call Verification only applies where the region asks for it. Everything else is the same for every client. ## Authentication [#authentication] Send your reseller key as a bearer token on every request. See [Authentication](/docs/get-started/authentication). ```http Authorization: Bearer YOUR_API_KEY ``` The key must belong to the reseller account owner or an admin on it. A teammate with viewer access is refused. ## Your clients [#your-clients] List everything under your account, with each organization's balance, rate, concurrency limit and users: ```http GET /api/v1/reseller/organizations ``` Create a client. This makes the user and their organization together, and links it to you: ```http POST /api/v1/reseller/users/add { "name": "Demo User", "email": "demo@example.com", "phone": "+15551234567", "password": "...", "cost_per_min": 0.20, "concurrent_call_limit": 2, "expiry_date": "2026-12-31", "user_currency": "USD" } ``` Only the first four are required. The rest are the terms you are selling on, and you can change them later. `cost_per_min` sets both the basic and the premium model rate for that client. `cost_per_min` has to be at least what you pay, for both rates. A lower value is rejected rather than quietly costing you money on every call the client makes. `welcome_minutes_to_credit` is capped the same way, at the minutes your own balance can cover. ## What a client can do [#what-a-client-can-do] Three things are yours to set, and a client cannot change any of them itself. **Dashboard access.** Turn menu areas on and off. Only the flags you pass change, and flags outside your own permissions are ignored rather than escalating. ```http POST /api/v1/reseller/users/access-control { "user_id": 1234, "dashboard_menu_access": { "is_bots_menu_access": true } } ``` **Expiry.** Set a date the account stops working, or clear it. ```http POST /api/v1/reseller/users/expiry { "user_id": 1234, "expiry_date": "2026-12-31" } ``` Pass `null` to remove the expiry. **Concurrency.** How many calls a client can run at once. ```http POST /api/v1/reseller/concurrency { "child_organization_id": 5678, "new_limit": 5 } ``` `new_limit` is the absolute number you want, not a change to the current one. Assigning is free: any figure is accepted, nothing is deducted from you, and the call never fails for lack of capacity. Your own concurrent call limit is the ceiling for your whole family at dial time. What you set per client is a cap; your own limit is the capacity those caps compete for. Give five clients ten each while you hold twenty, and the twenty-first simultaneous call is refused no matter whose it is. ## Credits [#credits] You hold a balance. You move minutes from it to a client at a rate you choose, and you can take unused minutes back. Two different amounts move on every transfer: your balance is debited at **your** rate, and the client is credited at the rate **you set**. The gap is your margin. A revert is the exact mirror. your balance the client transfer: you pay your rate, they get your sell rate revert: they are deducted at their rate, you get yours back A revert needs no rate from you: the client is deducted at their current rate, which is what they were charged, and you are refunded at yours, which is what it cost you. Preview either direction before committing. Nothing moves on this call: ```http POST /api/v1/reseller/credits/calculate { "minutes": 100, "cost_per_min": 0.20 } ``` Then transfer. Your balance is debited immediately, and a `cost_per_min` below your own rate is refused as a loss: ```http POST /api/v1/reseller/credits/transfer { "to_organization_id": 5678, "minutes": 100, "cost_per_min": 0.20 } ``` To take unused minutes back, pass only how many. No rate: the client is deducted at their current rate and you are refunded at yours: ```http POST /api/v1/reseller/credits/revert { "from_organization_id": 5678, "minutes": 40 } ``` Every move is recorded: ```http GET /api/v1/reseller/credits/logs ``` ## Verify a client [#verify-a-client] Where a region requires it, a client has to be identity-verified before it can buy a number. For India that is an Aadhaar eKYC, so each check clears in minutes. You do not hardcode the order of the checks. You ask which step is next, run it, and the response tells you the one after it. That way the sequence can change without breaking your integration. Read the status once to get the first step, then follow `next_step` until the status is `completed`. GET /kyc/status the first next_step {"POST /kyc/steps/{next_step}"} next_step no next step status: completed the client can buy a number One read to start, then each step response hands you the next one. When a step comes back `completed` , verification is done and the client can buy. That is the same state the purchase endpoint checks, so there is nothing else to poll. ```http GET /api/v1/reseller/kyc/status?user_id=CLIENT_ID ``` The response has a `regions` array, one entry per region, each with that region's `status`, its `next_step`, and whether the client `can_purchase` yet. ```text status = GET /kyc/status?user_id=CLIENT_ID next = status.regions.find(r => r.region == "IN").next_step while next: step = POST /kyc/steps/{next} { user_id: CLIENT_ID, region: "IN", ...fields } next = step.next_step # status is completed. The client can now buy a number. ``` ### The steps [#the-steps] One endpoint runs every step: the step name is the last part of the path, and the fields it needs go in the body. India is the region with step-by-step verification today. Each step's body fields, the `next_step` it answers with, and the message it returns are listed in the [step reference](/docs/api-reference/reseller/submitResellerKycStep). You can also read the same thing as data, so your integration never hardcodes it: ```http GET /api/v1/reseller/kyc/requirements?region=IN ``` Two things the sequence does not show. `verify-gst` and `skip-gst` are a choice, run one or the other. And if the client says they did not get their code, call `resend-otp` while they are on the OTP step: it is never returned as a `next_step`, so it sits outside the loop. The client holds two codes at this step, one in their email inbox and one read out to them on a voice call, and `type` (`email`, `mobile`, or `both`, default `both`) picks which to send again. A client who says "I never got the call" needs `{ "type": "mobile" }`, nothing more. Roughly 30 seconds between attempts. A failed verify can burn the reference and a rapid resend trips the per-Aadhaar limit, which locks your client out, so a retry loop has to wait. Calling too soon returns `429`. ### Your obligation on the data [#your-obligation-on-the-data] You are relaying what your client types into your interface, nothing more. Do not store the values you send in these requests, and redact them from your logs. That covers the PAN, the Aadhaar number, and every one-time code. ## Buy them a number [#buy-them-a-number] Once a client is verified, buying is not a reseller endpoint: it is the ordinary [phone number API](/docs/buy-a-number-api) with your client's `user_id` added. The number lands on their account and the rental comes out of their balance, not yours. ```http GET /api/v1/phone_number/search?region=IN&user_id=CLIENT_ID POST /api/v1/phone_number/purchase { "region": "IN", "phone_number": "+91...", "user_id": CLIENT_ID } ``` A client who completed verification in the dashboard instead of through this API passes the same gate, so there is nothing to repeat. ## Reference [#reference] Full request and response schemas, with a playground, are in the [API reference](/docs/api-reference/reseller/listChildOrganizations). # OmniWorkflows (/docs/workflows) > Build visual automations that connect triggers to actions like voice AI calls, CRM updates, and messaging. OmniWorkflows is a visual, drag-and-drop automation builder. You connect a trigger (something that starts the flow) to a sequence of actions (calls, messages, CRM updates, and logic), so work happens automatically without writing code. OmniWorkflows is in beta and may need to be enabled for your account. If you do not see **Workflow** in the sidebar, contact support to turn it on. ## Where to find it [#where-to-find-it] In the dashboard sidebar under **Voice AI Setup**, open **Workflow**. From there you can create a workflow, open the builder, and view execution logs. ## What a workflow is made of [#what-a-workflow-is-made-of] A workflow starts with one trigger and then runs the actions you connect to it. ### Triggers [#triggers] * Webhook * OmniDim CRM * LeadSquared * Email event (replied, opened, clicked, or a delivery problem) * WhatsApp inbound * SMS received (Twilio) * Shopify * Instantly ### Actions [#actions] * **AI**: AI Assistant, Photo Analyzer * **Messaging and calls**: Send Email, Send SMS, WhatsApp, Voice AI call, Dialer, Add to Bulk Call * **CRM**: OmniDim CRM activity, LeadSquared update, LeadSquared get * **Integrations**: Custom API, Slack message, Enroll in Instantly * **Flow control**: Condition, Switch, Delay, Wait for time, Time condition, Run workflow (sub-workflow), Code, Note Provider actions (WhatsApp, Twilio SMS, Slack, Shopify, LeadSquared, Instantly, Voice AI) need the matching integration or credentials connected first. ## Build a workflow [#build-a-workflow] ### Create the workflow [#create-the-workflow] On the Workflow page, click to create a new workflow. Give it a name, optionally pick a folder, and start from a blank canvas or a template. ### Add and connect nodes [#add-and-connect-nodes] Drag nodes from the components palette onto the canvas and draw connections between them to set the order. A workflow has a single trigger at the start. ### Configure each node [#configure-each-node] Select a node to open its properties. To pass data forward, reference the output of an earlier node with a variable like `{{ context.. }}`. The upstream variables panel lists the fields each earlier node makes available so you can insert them. ### Save [#save] Save your work as a draft while you build. ## Test before you publish [#test-before-you-publish] * **Test the whole flow**: use **Test** in the top bar. For webhook and event triggers you paste a sample payload, which the run receives as `context.webhook`. The test opens the execution monitor so you can watch it run. * **Test a single node**: from a node's properties, run it on its own with manual input to check its output in isolation. ## Execution logs [#execution-logs] Open a workflow's executions to see each run. The live diagram color-codes nodes as completed, waiting, or failed. Click a node to inspect its input (data received from earlier nodes) and its output. Runs can be running, waiting, completed, failed, or cancelled, and you can cancel a run or rerun a failed one. ## Publish and activate [#publish-and-activate] Publishing makes the current version live and starts a fresh draft for further edits. A published workflow is read-only in the builder, with an active or paused toggle to turn the live version on or off. Webhook and event triggers only fire while a published version is active. ## Walkthrough: build a speed-to-lead workflow [#walkthrough-build-a-speed-to-lead-workflow] A common first workflow: when a new lead arrives, call them within seconds and log the outcome back to your CRM. ### Add the trigger [#add-the-trigger] Create a workflow and add a trigger. For speed-to-lead, use **OmniDim CRM** (fires when a lead is created) or **Webhook** (paste the generated webhook URL into your lead source so new leads post to it). ### Call the lead [#call-the-lead] Add a **Voice AI** action and connect it to the trigger. Choose the agent that qualifies leads and the number to call from. Map the lead's phone number from the trigger with a variable, for example `{{ context..phone }}`. ### Branch on the result [#branch-on-the-result] Add a **Condition** after the call. The Voice AI node exposes outputs like `call_status` and your extracted variables, so you can branch on something like `{{ context..extracted_variables.interested }}`. ### Act on each branch [#act-on-each-branch] On the interested branch, add an **OmniDim CRM activity** (or LeadSquared update) to log the disposition. On the other branch, add a **Send Email**, **WhatsApp**, or **SMS** follow-up. ### Test with a sample payload [#test-with-a-sample-payload] Click **Test** and paste a sample lead payload. It reaches the run as `context.webhook`. Watch the run in the execution monitor and check each node's input and output. ### Publish [#publish] Publish to make it live. Webhook and event triggers only fire on a published, active version. ## Common uses [#common-uses] * **Speed-to-lead**: a new lead (CRM event or webhook) triggers an instant Voice AI call, then writes the outcome back to your CRM. * **Follow-up messaging**: after a call, send a WhatsApp, SMS, or email based on the call result. * **Routing and enrichment**: use conditions and Custom API calls to branch the flow and pull in external data before the next step. ## Video walkthrough [#video-walkthrough] # Errors, rate limits, and retries (/docs/errors) > Error response shapes, status codes, rate limits, and retry guidance for the OmniDimension REST API. Every error from the API is JSON. No endpoint returns HTML, including unknown paths and wrong-method requests, so clients and AI agents can always parse the response. ## Error response shape [#error-response-shape] Most endpoints return errors in this shape: ```json { "error": "unauthorized", "error_description": "Missing or invalid API key" } ``` Call and campaign endpoints that report business outcomes use a `success` flag instead, with extra context where relevant: ```json { "success": false, "plan_expire": false, "error": "Concurrency call limit exceeded" } ``` `plan_expire: true` means the account balance or plan is the blocker; top up or upgrade, then retry. ## Status codes [#status-codes] | Code | Meaning | What to do | | ---- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | 400 | Invalid request: malformed JSON or missing required fields. `error_description` lists the missing fields. | Fix the request body. Do not retry unchanged. | | 401 | Missing or invalid API key. | Check the `Authorization: Bearer` header. Keys come from the dashboard. | | 404 | No endpoint at this path. The response includes `docs` and `openapi` links. | Check the path against the [OpenAPI spec](https://docs.omnidim.io/openapi.yaml). Do not retry. | | 405 | Path exists but not for this HTTP method. The `Allow` header lists valid methods. | Switch to a method from `Allow`. | | 429 | Rate limited. | Back off and retry after a minute. See limits below. | | 5xx | Server error. | Retry with exponential backoff. | ## Rate limits [#rate-limits] * Provider and voice listing endpoints (`/api/v1/voices`, provider lists): **10 requests per minute per user/IP**. Exceeding it returns a JSON 429; recently cached data may be served while limited. These lists change rarely, so cache them client-side. * Call dispatch is governed by your plan's **concurrency limit** (how many calls can run at once), not a request rate. Exceeding it returns `success: false` with a concurrency error; retry after active calls finish. * Other CRUD endpoints have no hard published rate limit. Be a good citizen: batch where the API offers it (bulk campaigns instead of loops over dispatch) and poll the [live-status endpoint](/docs/realtime) instead of refetching full campaign objects. ## Retry guidance for agents and SDKs [#retry-guidance-for-agents-and-sdks] * **429 or 5xx:** retry with exponential backoff and jitter, starting at 1 to 2 seconds. The voice-list 429 window is one minute. * **404:** do not blind-retry. Fetch the [OpenAPI spec](https://docs.omnidim.io/openapi.yaml) and re-resolve the endpoint name; LLM-remembered paths drift. * **405:** read the `Allow` header and switch methods. * **Dispatch retries:** there is no `Idempotency-Key` support yet, so a blind retry of `dispatchCall` can place a second call. Before retrying a timed-out dispatch, check [call logs](/docs/api-reference) for the original request's outcome. ## Service status [#service-status] Live availability for the API, omnidim.io, SIP infrastructure, and the MCP server: [status.omnidim.io](https://status.omnidim.io) The status page is also machine-readable. Agents and monitors should use the JSON endpoint instead of scraping the page: ```bash curl https://status.omnidim.io/api/status-page/heartbeat/all ``` Returns recent heartbeats and an `uptimeList` with per-service uptime ratios. Before retrying repeated 5xx errors, check it: if a service shows degraded, back off rather than hammering the API. ## Reference [#reference] Full request and response schemas, with a playground, are in the [API reference](/docs/api-reference). # Per-call languages (/docs/per-call-languages) > Set the languages an agent speaks on one specific call, chosen at dial time, without changing the agent's own configuration. An agent has a language list you configure once in the dashboard. Sometimes a single agent needs to serve people who speak different languages: a Tamil batch in the morning, a Hindi and English batch in the afternoon, a mixed list where every contact is different, or a web visitor who should be greeted in their own language. Send `__languages` when you start the call and it runs in the languages you name, without touching the agent's own settings and without affecting any other call in flight. It works anywhere you already send custom variables: a single dispatched call, a web call session, and a bulk campaign contact. If your contacts carry Indian postal codes rather than language names, send `__pincode` instead and the platform picks the languages for that state. ## What it changes [#what-it-changes] The languages you send **replace** the agent's configured list for that one call. They are not added to it. For the call you dialled, they decide: * how speech is transcribed, so the caller is understood * which languages the agent is allowed to reply in * how the agent follows the caller if they switch language mid-call * which language the call opens in Everything else about the agent (its prompt, voice, knowledge base, tools) is unchanged. ## Dialling one call [#dialling-one-call] Add `__languages` to the dispatch request. The first language in the list is the one the call opens in. ```json { "agent_id": 158910, "to_number": "+15551234567", "__languages": ["Tamil", "English"] } ``` The response tells you what was used: ```json { "success": true, "requestId": 91422, "status": "dispatched", "languages_applied": ["Tamil", "English"], "languages_ignored": [] } ``` A call is never rejected because of a language name. Any name that is not recognised is listed in `languages_ignored` and skipped. If none of the names are recognised, the agent's own languages are used and the call still goes out. You can also put `__languages` inside `call_context` if that suits your integration better. It behaves the same way, and it is never passed to the agent as a context variable. ## Starting a web call [#starting-a-web-call] Send it with the session's custom variables when you create the session. The visitor is greeted in the first language you name. ```json { "agent_id": 158910, "type": "voice", "custom_variables": { "name": "Rahul", "__languages": "Hindi|Gujarati|English" } } ``` Either form works here: a pipe-separated string as above, or an array. The key is removed before your other variables reach the agent, so `name` is passed through and `__languages` is not. ## Dialling a campaign [#dialling-a-campaign] Languages are set per contact, so one campaign can call each person in their own language. A retried or rescheduled contact keeps the languages it was first given. Add a `__languages` column to your file. Separate multiple languages with a pipe. Commas work too, but a pipe avoids fighting your spreadsheet's own comma handling. ```csv phone_number,name,__languages +15551234567,Rahul,Hindi|English +15559876543,Meera,Tamil +15550001111,Arjun, ``` Leave the cell empty for any contact who should use the agent's own languages. Every other column keeps working as a normal variable you can use in the agent's prompt. Add `__languages` to the contact, alongside your other fields. ```json { "contact_list": [ { "phone_number": "+15551234567", "name": "Rahul", "__languages": ["Hindi", "English"] }, { "phone_number": "+15559876543", "name": "Meera", "__languages": ["Tamil"] } ] } ``` The same key works when adding contacts to a dynamic campaign. ## Dialling by pincode [#dialling-by-pincode] If you hold Indian postal codes rather than language names, send `__pincode` instead of `__languages`. The platform reads the first two digits to decide the state and runs the call in that state's languages, so you do not have to keep a mapping of your own. The value must be a full six digit Indian PIN. Anything else is left alone: a five digit US ZIP, a partial code, or a stray number from another column will not be treated as a pincode, and the call runs on the agent's own languages. ```json { "agent_id": 158910, "to_number": "+15551234567", "__pincode": "700001" } ``` That call opens in Bengali. A contact in 600001 opens in Tamil. It goes everywhere `__languages` goes: the dispatch request, `custom_variables` on a web call session, a `__pincode` column in a bulk CSV, and a `__pincode` key on a JSON contact. Two things work differently from `__languages`: * **`__languages` wins.** If a contact carries both, the pincode is ignored for that call. That is the per-contact escape hatch: override the state mapping for one person without stripping their pincode. * **The pincode stays a variable.** Unlike `__languages`, it is not removed before your fields reach the agent, so the prompt can still reference where the customer is. `__pincode` is India only. A value that does not resolve to a state, such as an army post code, a typo, or a postal code from another country, leaves the call on the agent's own configured languages. It is never guessed at, and it does not fall back to Hindi or English. Twenty three states are mapped. **Goa is not**, because its PIN codes sit in the 403 range inside Maharashtra's `40` and it has no two digit prefix of its own; a Goa pincode therefore resolves to Maharashtra. Tell us if that matters for your traffic. ### What each state opens in [#what-each-state-opens-in] The first language listed is the one the call opens in. OmniDimension maintains this table, and the set for a state can be tuned. | State | PIN prefixes | Languages, opening language first | | ---------------- | ------------------------------ | ------------------------------------ | | Delhi | 11 | Hindi, English (India), Punjabi | | Haryana | 12, 13 | Hindi, English (India) | | Punjab | 14, 15, 16 | Punjabi, Hindi, English (India) | | Himachal Pradesh | 17 | Hindi, English (India) | | Jammu & Kashmir | 18, 19 | Urdu, Hindi, English (India) | | Uttar Pradesh | 20, 21, 22, 23, 25, 26, 27, 28 | Hindi, English (India), Urdu | | Uttarakhand | 24 | Hindi, English (India) | | Rajasthan | 30, 31, 32, 33, 34 | Hindi, English (India) | | Gujarat | 36, 37, 38, 39 | Gujarati, Hindi, English (India) | | Maharashtra | 40, 41, 42, 43, 44 | Marathi, Hindi, English (India) | | Madhya Pradesh | 45, 46, 47, 48 | Hindi, English (India) | | Chhattisgarh | 49 | Hindi, English (India) | | Telangana | 50 | Telugu, Hindi, Urdu, English (India) | | Andhra Pradesh | 51, 52, 53 | Telugu, Hindi, English (India) | | Karnataka | 56, 57, 58, 59 | Kannada, Hindi, English (India) | | Tamil Nadu | 60, 61, 62, 63, 64 | Tamil, English (India) | | Kerala | 67, 68, 69 | Malayalam, English (India), Hindi | | West Bengal | 70, 71, 72, 73, 74 | Bengali, Hindi, English (India) | | Odisha | 75, 76, 77 | Odia, Hindi, English (India) | | Assam | 78 | Bengali, Hindi, English (India) | | North East | 79 | English (India), Hindi | | Bihar | 80, 81, 84, 85 | Hindi, English (India), Urdu | | Jharkhand | 82, 83 | Hindi, English (India) | ## Parameter [#parameter] Use the same language names shown in your agent's language selector. See [Voices and languages](/docs/dashboard-guides/voices-and-languages) for the full list. Names are matched without case sensitivity, so `tamil`, `Tamil` and `TAMIL` are the same language. Common alternative spellings are understood too: `Bangla` for Bengali, `Oriya` for Odia, and `Panjabi` for Punjabi. The response echoes back the platform's own spelling of each name it applied. ## Which language the call opens in [#which-language-the-call-opens-in] The first language you send is the opening language. The agent greets the caller in it, and the conversation starts there. For the greeting itself to be spoken in that language, the agent's welcome message must be set to **dynamic**. A fixed welcome message is spoken exactly as written, in whatever language you wrote it in. If you use per-call languages, turn on the dynamic welcome message for that agent. With a fixed welcome message the greeting stays in its original language, though the rest of the conversation still follows the languages you sent. ## What the greeting will sound like [#what-the-greeting-will-sound-like] Your welcome message is treated as the **brief** for the opening line, not as words to translate. The agent reads what it is meant to achieve, who is calling, why, and what is being asked, then says that in the opening language the way a native speaker would actually say it on a phone call. This is deliberate. A word-for-word translation of an English line sounds translated, and the greeting is the first thing the caller hears. So a welcome message written as: > Hello, am I speaking with Rahul? This is Riya from Acme Finance about your > loan application. dialled with `__languages: ["Gujarati"]` produces a natural Gujarati greeting carrying the same three things: the caller's name, who is calling, and why. It will not be the same sentence rendered word for word. What stays fixed: * the same purpose and the same facts, with nothing added * no second greeting, no extra self-introduction, no small talk * at most one question * names, numbers and company names exactly as you wrote them * everyday English words a speaker of that language would normally use (appointment, payment, OK) stay in English If your welcome message is an **instruction** rather than words to say, for example "Greet with good morning or good evening based on the time of day", it is carried out in the opening language and never read out loud. Without `__languages`, nothing about your greeting changes. A call that does not set it still speaks your welcome message exactly as written. ## Before you rely on it [#before-you-rely-on-it] **Check the voice.** The agent's voice has to be able to speak the languages you send. A voice that only speaks English will not sound right delivering Tamil, whatever the language settings say. Multilingual voices are marked in the voice library. **Check the speech-to-text engine.** Soniox and Smallest accept a list of languages and follow whatever you send. Sarvam detects the language automatically and works as-is. Deepgram, Azure and Cartesia are configured with a single language on the agent and keep using it, so transcription on those engines does not follow the call's languages. If you dial in languages the agent was not built for, use Soniox or Smallest. **Send one language for a single-language call.** Sending one name pins the call to that language, exactly like an agent configured with one language. **Check the language is one the speech engine supports.** The agent can be configured with any language name, including regional ones such as Bhojpuri or Haryanvi, but the speech-to-text engines do not transcribe all of them. When a language has no equivalent on the engine, the agent still replies in it, but the caller's speech is transcribed without a hint for that language and accuracy drops. Soniox covers Bengali, Gujarati, Hindi, Kannada, Malayalam, Marathi, Punjabi, Tamil, Telugu and Urdu, but not Odia. Smallest covers the same list in streaming, minus Punjabi and Urdu, plus Odia. So an Odisha pincode, which opens in Odia, is transcribed on Smallest and not on Soniox. **Use Soniox for the four southern states.** Telangana, Andhra Pradesh, Karnataka and Kerala pair a southern language with Hindi, and that pair spans Smallest's regional auto-detect groups. A Smallest agent falls back to the southern language alone on those calls and loses Hindi and English transcription. Soniox handles every state in the table above correctly. ## Which speech-to-text engine to use [#which-speech-to-text-engine-to-use] The languages a call **replies** in always follow the pincode. What the agent can **hear** depends on the engine set on the agent, and the two engines cover different languages. | Language | Soniox | Smallest | | ----------- | ------ | -------- | | Bengali | Yes | Yes | | English | Yes | Yes | | Gujarati | Yes | Yes | | Hindi | Yes | Yes | | Kannada | Yes | Yes | | Malayalam | Yes | Yes | | Marathi | Yes | Yes | | **Odia** | **No** | Yes | | **Punjabi** | Yes | **No** | | Tamil | Yes | Yes | | Telugu | Yes | Yes | | **Urdu** | Yes | **No** | **Use Soniox.** It covers every language in the state table except Odia, and it transcribes a multi-language call without further constraints. On Soniox the only gap is Odisha, where the agent still speaks Odia but the caller's Odia is not transcribed with a hint for it. **Smallest has one more restriction.** It auto-detects only within fixed regional groups: one group is English, Hindi, Gujarati, Marathi, Bengali and Odia, another is English, Tamil, Telugu, Kannada and Malayalam. A call whose languages span both groups falls back to the first language alone. That affects four states in the table, because each pairs a southern language with Hindi: | State | On Smallest, transcription falls back to | | -------------- | ---------------------------------------- | | Telangana | Telugu only | | Andhra Pradesh | Telugu only | | Karnataka | Kannada only | | Kerala | Malayalam only | Hindi and English are still spoken on those calls, they are just not transcribed. Soniox has no such grouping. **Sarvam** detects the language on its own and needs no configuration here. **Deepgram, Azure and Cartesia** are configured with a single language on the agent and keep using it, so on those engines the caller is transcribed in the agent's configured language whatever the pincode says. ## Frequently asked questions [#frequently-asked-questions] No. The languages apply to the one call you dialled. The agent's saved configuration is untouched, and other calls running at the same time are unaffected. The name is ignored and the call still goes out. On a single dispatch the response lists it under `languages_ignored`. If every name is unrecognised, the call runs in the agent's own languages. Yes, within the languages you sent. If you send Tamil and English, the agent follows the caller between those two. It will not switch to a language you did not send. Yes. A retried or rescheduled contact is called with the same languages as the original attempt, so a callback continues in the language of the conversation it refers to. No. It is a dialling instruction, not a variable. It is removed before your other fields are passed to the agent, so it never appears in the conversation context. Anywhere you already send custom variables: a dispatched call, a web call session, and a bulk campaign contact. Not when you send `__languages`. Your welcome message becomes the brief for the opening line, and the agent composes a natural greeting in the opening language carrying the same purpose and facts. Word-for-word translation sounds translated, which is worse on the first line of a call. Calls without `__languages` are unaffected and still say your welcome message as written. No. The languages are chosen when the call is started, and an inbound caller arrives without that step, so inbound calls use the agent's configured languages. ## Related [#related] " title="Voices and languages" href="/docs/dashboard-guides/voices-and-languages" description="The languages and voices available, and how to configure them on an agent." /> " title="Bulk calls" href="/docs/bulk-calls/overview" description="Run a campaign from a contact list, with per-contact fields." /> " title="Dispatch call API" href="/docs/api-reference/calls/dispatchCall" description="Full request and response reference for dialling a single call." /> # Real-time and streaming (/docs/realtime) > How real-time data flows in OmniDimension. Streaming voice on live calls, webhook event delivery for call results, and live status polling for bulk campaigns. OmniDimension is a real-time product at the voice layer and an event-driven product at the API layer. This page maps each real-time need to the right mechanism. ## Streaming voice on live calls [#streaming-voice-on-live-calls] Every call runs on a streaming pipeline: caller audio is streamed to speech-to-text, the LLM response is generated and streamed to text-to-speech, and synthesized audio streams back over the call, with sub-200ms end-to-end latency. The same streaming pipeline powers phone calls, the web voice widget, and WhatsApp voice. You do not manage this stream yourself. Dispatch a call with the API, SDK, or MCP server and the platform handles the audio streaming. The same applies to the embeddable web voice widget: it streams audio between the visitor's browser and the agent in real time, and agents can run live web search inside a conversation when enabled, so answers can include current information mid-call. ## Live call monitoring [#live-call-monitoring] The dashboard streams active calls in real time: watch conversations as they happen across your agents. This is a dashboard surface (backed by a WebSocket service), not a public API; for programmatic visibility into running activity, use the bulk-call live status endpoint below or fetch call logs after completion. ## Webhooks: push delivery for call events [#webhooks-push-delivery-for-call-events] For machine-to-machine delivery of call results, use webhooks. When a call completes, OmniDimension POSTs your configured payload to your endpoint: transcript, summary, sentiment, extracted variables, and the recording URL. * Setup guide: [Post-call delivery](/docs/dashboard-guides/post-call) * Configure under **Post call** on your agent: choose Webhook, paste your endpoint URL, and define a custom payload with call variables. * Test with Webhook.site or RequestBin to inspect real payloads before pointing at production. Webhooks are the recommended pattern for agents and integrations that need results as soon as a call ends, instead of polling call logs. Webhook is one of several post-call delivery channels: the same completion event can also be delivered to Email, Salesforce, Slack, HubSpot, or WhatsApp Cloud, configured per agent. ## Polling: live status for bulk campaigns [#polling-live-status-for-bulk-campaigns] Running campaigns expose a lightweight live status endpoint designed for repeated polling. It returns aggregated counts in one cheap query: ```bash curl https://omnidim.io/api/v1/bulk-call/{bulk_call_id}/live-status \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` ```json { "status": "success", "bulk_call_id": 42, "campaign_status": "in_progress", "summary": { "total_contacts": 500, "queued": 120, "in_progress": 8, "completed": 350, "failed": 12, "busy": 6, "no_answer": 4 } } ``` Poll every few seconds while a campaign runs. For individual call results, prefer webhooks over polling call logs. ## Server-sent events (SSE) [#server-sent-events-sse] The REST API does not currently expose SSE or chunked streaming responses; API responses are standard JSON. Real-time delivery is covered by the mechanisms above: streaming voice inside calls, webhook push for call events, and the live-status polling endpoint for campaigns. If your use case needs SSE specifically, [contact us](https://omnidim.io/contact-us). ## Service status [#service-status] Live availability for the API, omnidim.io, SIP infrastructure, and the MCP server: [status.omnidim.io](https://status.omnidim.io). Agents can read it as JSON: `https://status.omnidim.io/api/status-page/heartbeat/all` (heartbeats and uptime per service). ## Choosing the right mechanism [#choosing-the-right-mechanism] | You need | Use | | -------------------------------------- | ------------------------------------------------------------------------ | | Live conversation with a caller | Dispatch a call; voice streaming is built in | | Live conversation on your website | Embed the web voice widget | | Watch active calls as they happen | Live call monitoring in the dashboard | | Call results pushed to your system | Post-call webhook (or Email, Salesforce, Slack, HubSpot, WhatsApp Cloud) | | Campaign progress for a dashboard | Bulk call live-status polling | | Individual call details after the fact | Call logs API | | Platform availability | status.omnidim.io, JSON at /api/status-page/heartbeat/all | ## Reference [#reference] Full request and response schemas, with playgrounds, are in the [Create session](/docs/api-reference/sessions/createSession), [Bulk call live status](/docs/api-reference/bulk-calls/getBulkCallLiveStatus), and [Call logs](/docs/api-reference/calls/listCallLogs) API references. # Web call protocol (/docs/web-call-protocol) > The WebSocket protocol behind web call sessions, for iOS, Android, Flutter, kiosks, and any platform without an official SDK. Web call sessions stream audio over a plain WebSocket with JSON frames. In the browser, the `@omnidim-ai/client` SDK (see the SDKs tab) handles all of this for you. Use this page when you are integrating from a platform without an official SDK: mobile apps, desktop apps, kiosks, or embedded devices. ## Get a connection URL [#get-a-connection-url] Create a session server-side with your API key using [create session](/docs/api-reference/sessions/createSession). The response contains a `ws_url`: ```json { "session_id": 4521, "token": "sess_51gF2qw8LxNz0vY4mT7Ka3RjD9pBcE6HuWiQnZsX0oM", "expires_at": "2026-07-17T12:15:00Z", "ws_url": "wss://live.omnidim.io/chat/start_voice_chat?request_token=sess_..." } ``` Connect your client to `ws_url` exactly as returned. Never construct or cache WebSocket URLs; the URL format is not a contract and can change at any time. Token semantics: * The token is valid to connect for 15 minutes after create. * It covers a single conversation and expires when that conversation ends. * If the connection drops mid-call, reconnecting to the same `ws_url` re-attaches to the same conversation, so a brief network blip does not lose the call. ## Send: microphone audio [#send-microphone-audio] Send one JSON frame per audio chunk: ```json { "type": "audio", "data": "" } ``` * Audio is 16 kHz, mono, 16-bit little-endian PCM, base64-encoded. * Chunks of roughly 100 to 200 ms work well. * Hang up by closing the socket. No other frame types are needed. ## Receive: agent events [#receive-agent-events] Every server frame is a JSON object with an `event` field. Ignore any event you do not recognize; new events may be added at any time. | Event | Payload | What to do | | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `media` | `media.payload`: base64 16 kHz mono PCM16 | Queue it for playback. | | `clear` | none | Stop playback and drop everything queued. The visitor interrupted the agent, so latency here is very visible. | | `end_call` | `media.payload.reason`: why the call ended (for example `hangup`) | Stop audio and close your UI. The server closes the socket. | | `user` | `media.payload`: text | Final transcript of the visitor's speech so far this turn. | | `partial_text` | `media.payload`: text | Interim (still changing) visitor transcript. | | `system` | `media.payload`: text | The agent's reply text as it starts speaking. | | `last_system_message` | `media.payload`: text | The agent's full reply so far this turn. Each one supersedes the previous, so render the latest. | | `error` | message text | Surface or log the error. The call may still continue. | ## Close codes [#close-codes] | Code | Meaning | | ---- | ----------------------------------------------------------------- | | 1000 | Normal close: the conversation ended. | | 1011 | Internal error. | | 1013 | Try again later: the platform is at capacity. Retry with backoff. | An invalid, expired, or already-used token closes the connection during setup. Create a new session and connect with the fresh `ws_url`. ## Reference [#reference] Full request and response schemas, with a playground, are in the [Create session API reference](/docs/api-reference/sessions/createSession). # API reference (/docs/api-reference) > REST + Python SDK reference for the OmniDimension platform with an interactive try-it-out playground. The OmniDimension API lets you create agents, dispatch calls, manage phone numbers, and pull call analytics from your own systems. Auth is a single Bearer API key on every request. Open any endpoint below and you will find a live playground. Paste your API key under **Authorization**, fill in the request, and hit **▶ Run** to see the real response without leaving the docs. Your key is held in this browser session only — it's wiped on reload. **Base URL**  ·  **Auth** `Authorization: Bearer ` ## Download the OpenAPI spec [#download-the-openapi-spec] Every endpoint, request, and response in this reference is generated from a single OpenAPI 3.1 specification. Download [`openapi.yaml`](/openapi.yaml) to generate clients, drive an agent, or import into Postman, Insomnia, or Stoplight. ## Endpoint groups [#endpoint-groups] " title="Agents" href="/docs/api-reference/agents/listAgents" description="Create, retrieve, update, and delete AI voice agents." /> " title="Calls" href="/docs/api-reference/calls/dispatchCall" description="Dispatch calls and retrieve call logs." /> " title="Bulk calls" href="/docs/api-reference/bulk-calls/fetchBulkCalls" description="Outbound campaigns. Schedule, retry, pause, resume, and monitor live status." /> " title="Sessions" href="/docs/api-reference/sessions/createSession" description="Create web call sessions: a short-lived token and WebSocket URL for voice in your own app." /> " title="Knowledge base" href="/docs/api-reference/knowledge-base/listKnowledgeBaseFiles" description="Upload, list, attach, detach, and delete PDF documents on agents." /> " title="Phone numbers" href="/docs/api-reference/phone-numbers/listPhoneNumbers" description="List, attach, detach, and import from Twilio, Exotel, or SIP." /> " title="Providers" href="/docs/api-reference/providers/listLLMProviders" description="LLM, voice, STT, and TTS provider catalog." /> " title="Simulation" href="/docs/api-reference/simulation/createSimulation" description="Run automated agent test simulations." /> " title="Reseller" href="/docs/api-reference/reseller/listChildOrganizations" description="Manage child organizations, transfer credits, and control user access. Reseller accounts only." /> # Run a campaign over the API (/docs/bulk-calls/api) > Build a campaign as a draft, rotate it across a pool of numbers, steer it while it runs, and read per-contact results. The dashboard and the API drive the same campaign engine. This page covers the parts that only matter when you are driving it yourself: building a campaign across several requests, rotating across numbers, and reading results back without pulling the whole campaign into memory. If you are running campaigns by hand, start with the [campaign guide](/docs/bulk-calls/overview) instead. [ Runnable code: this whole page as a Python and TypeScript CLI, printing each request before it sends. Open on GitHub ↗ ](https://github.com/Omnidim/examples/tree/main/bulk-campaigns) ## The shape of a campaign [#the-shape-of-a-campaign] A campaign created without `save_as_draft` starts dialing the moment it is created. That is fine when you have everything up front. When you don't, create it as a draft, build it up, and start it when it is ready. ### Create the campaign as a draft [#create-the-campaign-as-a-draft] ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/create" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "August follow-ups", "phone_number_id": "177", "save_as_draft": true, "concurrent_call_limit": 3 }' ``` Keep the `id` from the response. Every request below uses it. ### Add contacts in batches [#add-contacts-in-batches] Send up to 1000 per request rather than one contact per request. ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/add_contacts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ {"to_number": "+15551234567", "custom_variables": {"first_name": "Ravi"}}, {"to_number": "+15559876543", "custom_variables": {"first_name": "Priya"}} ] }' ``` Anything in `custom_variables` reaches the agent as context for that specific call, so the agent can greet Ravi by name. Rows that fail validation come back in `rejected` with the array index and a reason, and the rest are still added. A single bad number never costs you the batch. ### Set up number rotation [#set-up-number-rotation] Optional, and covered in [Rotating across numbers](#rotating-across-numbers) below. Skip it if you are dialing from one number. ### Start it [#start-it] ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/start" \ -H "Authorization: Bearer YOUR_API_KEY" ``` The contact key is not the same on every endpoint. `contact_list` on [Create bulk call](/docs/api-reference/bulk-calls/createBulkCall) uses `phone_number` with loose keys alongside it. `add_contact` and `add_contacts` use `to_number` with variables inside an explicit `custom_variables` object. ## Rotating across numbers [#rotating-across-numbers] One number dialing an entire campaign gets reported as spam and stops being answered. Carriers score numbers on how they are used, and a number that places hundreds of unanswered calls a day degrades on its own. A pool spreads that load and rotation moves off a number before it burns out. Everything rotation needs goes in one `rotation` object at creation: ```json { "name": "August follow-ups", "phone_number_id": "177", "save_as_draft": true, "rotation": { "numbers": [ {"phone_number_id": 177, "sequence": 10}, {"phone_number_id": 178, "sequence": 20}, {"phone_number_id": 179, "sequence": 30} ], "strategy": "fixed_count", "calls_per_number": 50 } } ``` The policy and the numbers it applies to live in the same object on purpose. You cannot ask for rotation without saying what to rotate across, so the commonest way to get this wrong does not exist. `sequence` is just the dialing order, lowest first. `1, 2, 3` works exactly like `10, 20, 30`; any integers do. The tens are only a habit that leaves room to slot a number in between later without renumbering the rest. ### How the pool relates to `phone_number_id` [#how-the-pool-relates-to-phone_number_id] `phone_number_id` is always required, but what it does depends on whether you send a `rotation` object: | Request | What dials | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | No `rotation` | `phone_number_id` makes every call. | | With `rotation` | The numbers in `rotation.numbers` make the calls. `phone_number_id` goes on standby and is used only if every rotation number ends up paused. | Sending a number as `phone_number_id` does **not** put it in the rotation. That is why the example lists 177 in both places: once as the campaign's number, once in `numbers` so it takes a share of the calls. Leave it out of `numbers` and it sits in reserve. Verified behaviour, not a guess: with a rotation of one other number, a campaign whose `phone_number_id` was `+15550009001` dialled from `+15550009002`, and only fell back to `+15550009001` once every rotation number was paused. ### Choosing a strategy [#choosing-a-strategy] `strategy` defaults to `fixed_count`, so a rotation block with just `numbers` in it rotates sensibly rather than sitting on the first number. When every number falls below the threshold, `fallback` decides what happens: `pause` stops the campaign so you can add fresh numbers, `continue_best` keeps dialing with the healthiest one you have. ### What happens to the agent on your numbers [#what-happens-to-the-agent-on-your-numbers] A rotation number has to answer as the campaign's agent. What that means depends on where the number is pointed already: | The number's current agent | What happens | | -------------------------- | ---------------------------------------------------------------------------- | | None | The campaign's agent is attached to it automatically. Nothing for you to do. | | The same agent | Nothing changes. | | A different agent | The request is refused and names the number. | The last row is refused rather than repointed because the number would otherwise sit in your rotation while still belonging to another agent. Either attach it to this campaign's agent first, or leave it out. The same rule applies to `bot_id`: if you name an agent that contradicts the agent on `phone_number_id`, the request is refused rather than one of them quietly winning. Attaching an agent to a number affects more than this campaign. The attached agent also answers that number's inbound WhatsApp and SMS. It does not change inbound voice. ### Watching rotation happen [#watching-rotation-happen] [List rotation pool](/docs/api-reference/bulk-calls/listBulkCallNumbers) shows which number is dialing right now and how far into its cycle it is: ```json { "rotation": {"strategy": "fixed_count", "calls_per_number": 50}, "numbers": [ { "assignment_id": 508, "phone_number": "+15551234567", "is_active": true, "is_dialing_now": true, "calls_dispatched": 124, "calls_this_cycle": 24, "health_score": 82.5 } ] } ``` Watch `calls_this_cycle`, not `calls_dispatched`. The cycle count is what `fixed_count` compares against `calls_per_number`, so at 24 of 50 this number has 26 calls left before rotation. `calls_dispatched` is its lifetime total across every cycle it has had. ## Reading results [#reading-results] [Bulk call results](/docs/api-reference/bulk-calls/listBulkCallLines) returns one row per contact: what happened, the variables you sent, and a pointer to the recording. ### Paging [#paging] There is one rule. Call it, then keep passing back the `next_cursor` you were handed until it comes back `null`. ```python cursor = None while True: page = get(f"/api/v1/calls/bulk_call/314/lines", params={"pagesize": 150, "cursor": cursor}).json() for row in page["records"]: handle(row) cursor = page["next_cursor"] if not cursor: break ``` Send no cursor on the first request. Cursors are opaque: pass back the string you were given and never build one yourself. Each call returns a page of rows, oldest first: `pagesize` goes up to 150 and defaults to 30, so a 1,000-contact campaign reads back in 7 requests. No contact is skipped or returned twice, even while the campaign is still dialing. `include_total=true` adds `total_records`, at the cost of a count across the whole filtered campaign. Ask for it once to fill a header, not on every page. ### Getting a transcript [#getting-a-transcript] Rows carry `call.recording_id`, not the conversation. Transcripts reach 212 KB, so a full page of them would be tens of megabytes. Fetch the ones you want: ```bash curl "https://backend.omnidim.io/api/v1/calls/logs/50585" \ -H "Authorization: Bearer YOUR_API_KEY" ``` A row whose `call` is `null` has not been dialed yet. ### Polling a running campaign [#polling-a-running-campaign] For a progress bar, use [Bulk call live status](/docs/api-reference/bulk-calls/getBulkCallLiveStatus) rather than walking the results. It returns aggregate counts in one query and does not grow with the campaign. ## Steering a campaign while it runs [#steering-a-campaign-while-it-runs] | You want to | Call | | ---------------------------------------- | ------------------------------------------------------------------------------- | | Speed up or slow down | [Change concurrency](/docs/api-reference/bulk-calls/setBulkCallConcurrency) | | Stop dialing from a number going bad | [Pause a pool number](/docs/api-reference/bulk-calls/setBulkCallNumberActive) | | Bring in a fresh number | [Add number to pool](/docs/api-reference/bulk-calls/addBulkCallNumber) | | Re-queue contacts that did not connect | [Retry](/docs/api-reference/bulk-calls/retryBulkCall) | | Hold overnight and resume in the morning | [Set calling hours](/docs/api-reference/bulk-calls/setBulkCallDailyTimeControl) | Pausing a number is usually better than removing it: dialing moves to the next number in sequence, and the paused number keeps its counters and history in case you want it back. You cannot pause the last active number of a running campaign, since the campaign would have nothing left to dial from. ## Filtering contacts before dialing [#filtering-contacts-before-dialing] `call_conditions` lets you hand over a whole list and have the campaign decide who to actually call. It exists so you do not have to filter your export first, and so the decision is recorded rather than lost in a spreadsheet somewhere. ### The mental model [#the-mental-model] Three things to hold, and it stops being confusing: 1. **A condition tests one column on one contact row.** `column` is the key on the row, not a field of ours. 2. **All conditions must pass.** They are ANDed. There is no OR. 3. **A contact that fails is kept, not deleted.** It lands with `call_status: "Skipped"`, so the results still show it and you can check the filter did what you meant. ### A worked example [#a-worked-example] Say you export every open account from your CRM, but you only want to call customers on the pro plan who owe more than 100. Send the whole export: ```json { "name": "Renewals sweep", "phone_number_id": "177", "call_conditions": [ {"column": "plan", "operator": "equals", "value": "pro"}, {"column": "balance", "operator": "greater_than", "value": "100"} ], "contact_list": [ {"phone_number": "+15551110001", "plan": "pro", "balance": "240"}, {"phone_number": "+15551110002", "plan": "free", "balance": "900"}, {"phone_number": "+15551110003", "plan": "pro", "balance": "12"} ] } ``` `plan` and `balance` are columns you invented on your own rows. Both conditions have to pass, so only the first contact is called: | Contact | plan | balance | Result | | ------------ | ---- | ------- | -------------------------- | | +15551110001 | pro | 240 | Called | | +15551110002 | free | 900 | `Skipped`, wrong plan | | +15551110003 | pro | 12 | `Skipped`, balance too low | The create response tells you the split before a single call goes out: ```json "filtering_stats": { "total_contacts": 3, "filtered_contacts": 1, "skipped_contacts": 2, "filtered_percentage": 33 } ``` Worth reading. If you meant to call most of your list and this says 5%, you have a typo in a column name rather than a very selective campaign. ### The operators [#the-operators] | Operator | Passes when | Example | | -------------- | --------------------------- | ------------------------------ | | `equals` | Exact match | `plan` equals `pro` | | `not_equals` | Anything but that | `status` not\_equals `churned` | | `contains` | Substring, case-insensitive | `email` contains `@acme.` | | `greater_than` | Numerically greater | `balance` greater\_than `100` | | `less_than` | Numerically less | `days_overdue` less\_than `90` | `value` is always sent as a string, including for the numeric two: send `"100"`, not `100`. A row whose value is not a number fails a numeric comparison rather than erroring, so a stray `"balance": "n/a"` skips that one contact instead of failing the whole request. ### One difference on add\_contacts [#one-difference-on-add_contacts] On [Add contacts in bulk](/docs/api-reference/bulk-calls/addBulkCallContacts), a contact whose `custom_variables` do not contain the condition's `column` **at all** is rejected rather than added as `Skipped`. A missing column cannot be judged either way, and dialing it on a guess is worse than saying so. Send every column your conditions name. ## Things that catch people out [#things-that-catch-people-out] Check `strategy` is not `none`, and that the other numbers are not paused. `GET /numbers` shows `is_active` and `is_dialing_now` for each. Rotation also only moves on when its trigger fires, so with `calls_per_number: 50` the first number takes 50 calls before anything changes. `retry_limit` must be between 1 and 10. Do not send `0` to mean "no retries": leave `auto_retry` as `false` and omit `retry_limit` entirely. That is deliberate. Repeated numbers are kept rather than merged, because two rows for one number usually means two real reasons to call, often with different variables. If you want one call, deduplicate before sending. Cursors are opaque and are validated. Pass back exactly the `next_cursor` string you received, or omit it to start over from the beginning. The number either belongs to another account, is already in the rotation, is listed twice in the same request, or is attached to a different agent. The error names which. Read the `Skipped` rows back from [Bulk call results](/docs/api-reference/bulk-calls/listBulkCallLines) and look at their `custom_variables`. Usually the column name does not match what is on the rows, or a numeric comparison is hitting values that are not numbers. Conditions are ANDed, so one wrong condition skips everything. # Bulk Outbound Call Best Practices (/docs/bulk-calls/best-practices) > Complete guide for optimizing bulk call campaigns, from agent configuration to post-call analysis. Optimization playbook for bulk outbound call campaigns. Covers agent configuration, conversation tuning, scheduling, retries, scaling, and analytics. Bulk outbound call best practices ## Agent configuration [#agent-configuration] Essential settings for optimal agent performance in bulk outbound campaigns. ### Welcome message [#welcome-message] * Keep it short and concise. e.g., "Hello, am I speaking with Aman?" * Add personalization using variables (e.g., `[name]`, `[company]`) * State purpose clearly after user acknowledges the call * Test different variations for optimal response rates Welcome message configuration ### Prompting best practices [#prompting-best-practices] * Start with simple prompts → test gradually → add scenarios and conditions * Use one-shot and few-shot prompting for LLM efficiency. Example: "You are a sales agent. When user says they're busy, respond: I understand you're busy. Would 2 minutes next Tuesday work better?" * For multi-lingual campaigns, include language-specific prompting guidelines. Example: "If user responds in Spanish, continue conversation in Spanish with appropriate cultural context" * Add fallback instructions for unexpected user responses. Example: "If you don't understand the user's response, say: I want to make sure I understand you correctly. Could you help me clarify that?" ### TTS-friendly response generation [#tts-friendly-response-generation] * Write dates and numbers in spoken format * Example 1, Good: "January fifth, twenty twenty-five" | Bad: "01/05/2025" * Example 2, Good: "twenty-five dollars" | Bad: "$25" * Example 3, Good: "CRM (see-are-em)", "API (ay-pee-eye)", "SQL (sequel)" ### Prompting guide for KB integration [#prompting-guide-for-kb-integration] * Define specific triggers when the Knowledge Base should be consulted Knowledge base integration ## Configurations [#configurations] Configuration settings that impact call quality and user experience. ### Silence timeout [#silence-timeout] * Time to wait after speech ends before generating a response * Recommended: 300 ms (0.3 seconds) for optimal performance * Adjust based on target demographic (e.g., older users need more time) * Test different timeouts with pilot campaigns ### Interruption sensitivity [#interruption-sensitivity] * Controls how quickly the assistant stops speaking when the user starts talking * If speech doesn't reach the set threshold (ms), audio is ignored * 150 ms (high sensitivity): very sensitive, may trigger on background speech * 600 ms – 1s (medium): balanced, best for natural conversations * 1000 ms – 3000 ms (low): less sensitive, may miss short replies ("yes", "no") * Start with 600–1000 ms for most cases * Test interruption handling in different scenarios to find optimal setting Silence timeout settings ### Noise handling [#noise-handling] * Apply noise reducer to minimize background environmental sounds (fan, traffic, etc.) * Does not cancel out active conversations or background talking * Test with different ambient noise scenarios ### Language model settings [#language-model-settings] * Choose LLM based on conversation complexity and speed needs * Start with GPT-4o-mini for balanced performance * Enable streaming for real-time, low-latency responses * Set temperature: 0.2–0.4 for factual, 0.5–0.7 for natural / balanced tone * Test different models with real conversation scenarios * Continuously monitor trade-offs between response quality and speed Language model settings ### Voice selection [#voice-selection] * Choose voice that matches your brand personality and target audience * Consider regional accents for local market relevance * Test voice clarity and naturalness with sample conversations * A/B test different voices for optimal engagement rates * Consider gender preferences based on campaign type and audience Voice selection ### Background noise simulation [#background-noise-simulation] * Add subtle background noise for more natural feel (optional) * Choose appropriate environment sounds (office, restaurant, etc.) * Keep volume low to avoid distraction from main conversation (e.g., 0.30) * Test impact on call quality and user perception Background noise simulation ## Post-call handling [#post-call-handling] Comprehensive data extraction and follow-up processes for maximum campaign value. * Save complete transcription for quality analysis and compliance * Generate structured call summary with key points and outcomes * Extract predefined variables relevant to campaign objectives * Example 1, lead qualification: hot lead, warm lead, cold lead, not qualified * Example 2, intent level: high interest, moderate interest, low interest, not interested * Add Google Sheet post-call for data analysis and reporting Post-call handling ## Bulk call guidelines [#bulk-call-guidelines] Strategic approach for successful bulk campaign execution. ### Campaign management [#campaign-management] * Use descriptive, date-stamped campaign names (e.g., `Q3_Product_Launch_East_Coast_2024`) * Include context columns matching agent variables (name, company, industry, etc.) * Configure timezone-aware scheduling for optimal call timing ### Call rescheduling and retry [#call-rescheduling-and-retry] Optimizing follow-up strategies for maximum coverage and compliance. #### Rescheduling configuration [#rescheduling-configuration] * Update timezone handling for accurate scheduling across regions * Add specific prompts for handling rescheduling requests naturally * Example: if customer requests rescheduling, ask for the new date and time to call back Timezone configuration #### Retry strategy [#retry-strategy] * Configure maximum retry attempts per number (typically 2–3 times) * Space retries appropriately: 24–48 hours between attempts Auto-retry configuration ## How to go live with bulk calls [#how-to-go-live-with-bulk-calls] Strategic approach for successful bulk campaign execution. ### Pilot internal testing [#pilot-internal-testing] * Start with 5–10 internal test calls using sample numbers * Test different conversation scenarios and edge cases * Verify agent responses to common objections and questions * Check technical functionality: call quality, data extraction, integrations * Document issues and optimize before real user testing ### Small batch rollout [#small-batch-rollout] * Dispatch initial batch of \~200 calls to real prospects * Monitor calls in real-time during initial hours * Track key metrics: pickup rate, conversation length, completion rate, success rate, etc. * Collect immediate feedback from answered calls * Pause campaign if major issues are detected * Analyze results before proceeding to larger volumes * Optimize based on real-world performance data ### Scaling approach [#scaling-approach] * Scale gradually: 200 → 500 → 1000 → larger volumes * Wait for performance stabilization before each scaling step * Monitor system performance and call quality at each scale ## Analysis and optimization [#analysis-and-optimization] Data-driven approach to continuous campaign improvement. Analysis and optimization ### Measure key metrics [#measure-key-metrics] * **Pickup Rate**: track by time of day, day of week, lead source, geography * **Conversation Duration**: average length, completion rate, early hang-ups * **Interaction Count**: back-and-forth exchanges indicating engagement level * **Conversion Rate**: percentage achieving primary campaign objective * **Lead Quality Score**: hot / warm / cold lead distribution from calls * **Agent Performance**: response accuracy, objection handling, flow adherence * **Technical Metrics**: call quality, connection success, system performance ### Diagnose issues [#diagnose-issues] Common scenarios and their specific optimization strategies. #### Scenario A: Low pickup rate (\< 20%) [#scenario-a-low-pickup-rate--20] * Analyze lead quality: source, age, verification status * Optimize call timing: test different hours, days of week, seasonal patterns * Implement local number presence for better pickup rates * Analyze geographic and demographic pickup patterns #### Scenario B: Good pickup (> 30%) but low interactions (\< 3 exchanges) [#scenario-b-good-pickup--30-but-low-interactions--3-exchanges] * Simplify opening conversation flow and reduce complexity * Shorten bot questions and responses for better engagement * Clarify value proposition in opening line within first 10 seconds * Reduce cognitive load with simpler language and concepts * Test different conversation pacing and natural pauses * Improve interruption handling and conversation recovery * A/B test different opening scripts and value propositions #### Scenario C: High pickup and interactions but low conversion (\< 10%) [#scenario-c-high-pickup-and-interactions-but-low-conversion--10] * Analyze conversation quality issues in detail * **Objection handling**: review common objections and response effectiveness * **Interruption management**: ensure natural conversation flow recovery * **Clarification requests**: improve agent's ability to understand and respond * **Value communication**: strengthen benefit articulation and relevance * **Call-to-action clarity**: make next steps obvious and compelling * **Trust building**: enhance credibility indicators and social proof * **Closing techniques**: improve commitment and follow-through processes ### Optimize conversation design [#optimize-conversation-design] * Adjust prompts based on actual conversation patterns and outcomes * Improve objection handling with real examples from call analysis * Enhance agent training data with successful conversation examples * Test new conversation flows with A/B testing methodology ### Iterate and scale [#iterate-and-scale] * Run new test batch with updated conversation flow and configuration * Compare performance metrics against baseline from previous iterations * Continue optimization cycles until metrics stabilize at acceptable levels # Dynamic Campaign Guide (/docs/bulk-calls/dynamic) > Create a Dynamic Campaign that accepts contacts in real time via a webhook API. Ideal for CRM integrations and continuous outreach. A Dynamic Campaign is a special type of bulk call campaign where contacts are added in real time through an API webhook, instead of being uploaded all at once via a CSV file. * Stays alive indefinitely; never auto-completes when the queue empties * Accepts new contacts at any time via a simple HTTP POST webhook * Transitions to **Waiting** status when the queue is empty, then resumes automatically when a new contact arrives * Perfect for CRM integrations, real-time lead pipelines, or always-on outreach campaigns * Supports the same scheduling, auto-retry, and call-rescheduling features as regular campaigns ## Prerequisites [#prerequisites] Before creating a Dynamic Campaign, make sure the following are in place. * A phone number in your dashboard. [Buy one directly from the Number Shop](/docs/dashboard-guides/numbers-shop) or import your own (Twilio, etc.) * An AI agent configured and attached to that phone number * An API key for authenticating webhook requests. Find it in the sidebar under **Account & Billing → API** * The external system (CRM, automation platform, etc.) that will send contacts to the webhook ## Creating a Dynamic Campaign [#creating-a-dynamic-campaign] ### Campaign details and phone number [#campaign-details-and-phone-number] Start the campaign creation wizard. Enter a campaign name and select the phone number with the attached agent. * Navigate to **Bulk Call** from the dashboard sidebar * Click **Create New Campaign** * Enter a descriptive campaign name (e.g., "Real-Time Lead Outreach") * Select your phone number from the dropdown. The attached agent will be shown automatically * Click **Next** to continue Step 1: Campaign Details ### Enable dynamic mode [#enable-dynamic-mode] On the contact upload step, switch from CSV upload to Dynamic Campaign mode. * You will see two options: **Upload CSV File** and **Dynamic Campaign (Add contacts via API/Webhooks)** * Select **Dynamic Campaign** * No CSV file is required. Contacts will be added later through the webhook * Optionally configure **Call Conditions** to automatically filter which contacts get called * Click **Next** to continue Step 2: Enable Dynamic Mode ### Scheduling and retry settings [#scheduling-and-retry-settings] Configure when the campaign runs and set up auto-retry for failed calls. * Choose **Start Immediately** or **Schedule for Later** with a specific date, time, and timezone * Enable **Daily Hard Stop** to pause calling outside of business hours * Enable **Daily Auto Start** to automatically resume calls each morning * Turn on Auto Retry and set a retry limit (up to 6 attempts) for failed calls * Enable Call Rescheduling to let the AI handle "call me back later" requests automatically * Click **Next** to review Step 3: Scheduling and retry ### Review and create [#review-and-create] Review all campaign settings before launching the Dynamic Campaign. * Confirm the campaign name, phone number, and agent * Verify the campaign type is shown as **Dynamic Campaign** * Check scheduling and retry configuration * Click **Create Campaign**. The campaign will start and wait for contacts via webhook Step 4: Review and create ## Creating a Dynamic Campaign via API [#creating-a-dynamic-campaign-via-api] You can also create a Dynamic Campaign directly from the API, without the dashboard wizard. Call [Create bulk call](/docs/api-reference/bulk-calls/createBulkCall) with `is_dynamic` set to `true`. `contact_list` is optional, so you can start the campaign empty and feed it entirely through the webhook. ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/create" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Real-Time Lead Outreach", "phone_number_id": "1", "is_dynamic": true }' ``` The response includes the new campaign `id` and a `current_status` of `waiting`. Use that `id` as the `{campaign_id}` in the [Add contact to dynamic campaign](/docs/api-reference/bulk-calls/addBulkCallContact) webhook to start pushing contacts. ## Campaign status flow [#campaign-status-flow] After creation, a Dynamic Campaign cycles through these statuses automatically. * **In Progress**: actively making calls from the queue * **Waiting**: queue is empty; campaign is idle but alive, ready to resume when a contact arrives * **Auto Paused**: outside operating hours (if daily hard stop is configured) * **Paused**: manually paused by the user * A Dynamic Campaign will never move to **Completed**; it stays alive indefinitely Campaign status flow ## Adding contacts via webhook [#adding-contacts-via-webhook] Once the campaign is created, use the webhook API to push contacts from any external system. * Find the Campaign ID on the campaign detail page * Send a POST request to the webhook endpoint with the contact's phone number * Include `custom_variables` to pass context to the AI agent (e.g., name, reason for call) * Include `metadata` for your own tracking (not shared with the agent) * The campaign will immediately start calling if it is in **Waiting** or **In Progress** status Webhook integration details See the [Add contact to dynamic campaign](/docs/api-reference/bulk-calls/addBulkCallContact) API reference for the full request and response schema. ### Webhook request format [#webhook-request-format] * **Endpoint**: `POST /api/v1/calls/bulk_call/{campaign_id}/add_contact` * **Header**: `Authorization: Bearer YOUR_API_KEY` * **Body** `to_number`: The contact's phone number in E.164 format (e.g., `+15551234567`) * **Body** `custom_variables`: Key-value pairs injected into the agent's conversation context * **Body** `metadata`: Key-value pairs stored for tracking purposes only (hidden from agent) ### Example: cURL [#example-curl] ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/123/add_contact" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to_number": "+15551234567", "custom_variables": { "name": "Jane Doe", "interest": "Home Insurance" }, "metadata": { "crm_lead_id": "lead_9876", "source": "website_form" } }' ``` ### Webhook response [#webhook-response] A successful request returns the updated campaign status and the new contact's line ID. * `status: "success"` confirms the contact was accepted * `campaign_status` shows the current campaign state (e.g., `in_progress` or `waiting`) * `line_id` is the unique ID of the new contact record in this campaign * If the campaign is outside operating hours, the contact is queued and called when the campaign resumes ## Campaign management [#campaign-management] You can control a Dynamic Campaign at any time from the campaign detail page. * **Pause**: temporarily stop all outbound calls (contacts added while paused will be called on resume) * **Resume**: restart a paused campaign * **Cancel**: permanently end the campaign; no further contacts will be accepted * **Update Retry Settings**: change retry limits and schedule without recreating the campaign * **Update Reschedule Settings**: enable or disable AI-driven call rescheduling mid-campaign ## Best practices [#best-practices] * Use meaningful `custom_variables` that match variables in your agent's welcome message or prompt * Set daily operating hours to avoid calling contacts outside business hours * Enable auto-retry to recover from unanswered or failed calls without manual intervention * Use the `metadata` field to store CRM or tracking IDs so you can correlate call results with your records * Monitor the campaign status via the dashboard or poll `/api/v1/calls/bulk_call/{id}` for real-time status * Pause the campaign before making major agent changes to avoid inconsistent conversations mid-campaign # Bulk calls (/docs/bulk-calls) > Run AI-powered outbound campaigns with retry logic, dynamic queues, and best practices for production deployment. Run AI-powered outbound calling campaigns at scale. Choose between CSV-uploaded campaigns or webhook-driven Dynamic Campaigns for real-time contact delivery. - [Bulk Call Campaign Guide](/docs/bulk-calls/overview) — Step-by-step walkthrough of creating a bulk campaign from CSV, scheduling, and managing it end to end. - [Dynamic Campaign Guide](/docs/bulk-calls/dynamic) — Long-running campaigns that accept contacts in real time via a webhook API. Ideal for CRM integrations. - [Run a campaign over the API](/docs/bulk-calls/api) — Build a campaign as a draft, rotate across a pool of numbers, steer it while it runs, and read per-contact results. - [Best practices](/docs/bulk-calls/best-practices) — Optimization playbook: agent configuration, scheduling, retries, scaling, and analytics. # Bulk Call Campaign Guide (/docs/bulk-calls/overview) > Complete step-by-step guide to create and manage AI-powered bulk call campaigns to reach multiple contacts efficiently. A complete walkthrough of creating, managing, and analyzing AI-powered bulk call campaigns from the OmniDimension dashboard. ## Prerequisites [#prerequisites] Before creating a bulk call campaign, you need to set up your phone number and deploy an AI agent. * Navigate to the **Phone Numbers** section from your dashboard * Import a phone number (e.g., Twilio, Exotel) * Ensure your agent is properly configured with context, instructions, and integrations * Attach the agent to your imported phone number * Test your agent with a single call before running bulk campaigns ### Deploy an agent with a phone number [#deploy-an-agent-with-a-phone-number] Deploy agent with phone number ### Agent successfully attached [#agent-successfully-attached] Agent attached confirmation ## Creating a bulk call campaign [#creating-a-bulk-call-campaign] The bulk call creation process is divided into 4 steps. Navigate to the **Bulk Call** section from your dashboard and click **Create New Campaign** to begin. ### Campaign details and phone number selection [#campaign-details-and-phone-number-selection] Set up your campaign name and choose the phone number with attached agent for making calls. * Enter a campaign name. Use descriptive names for easy tracking (e.g., "Q4 Product Launch Outreach") * Select a phone number from the dropdown * Review the agent details displayed automatically, including integrations and post-call actions * Verify the agent configuration is correct for your campaign goals * Click **Next** to proceed to Step 2 Step 1: Campaign details ### Upload contact list and CSV configuration [#upload-contact-list-and-csv-configuration] Upload your contact list in CSV format and configure call conditions. * Click **Upload CSV File** and select your contact list * The system will automatically detect columns and validate the format * Review the preview of your uploaded data * Set optional call conditions to filter which contacts to call * If missing variables are detected, download the template and add required data Step 2: Upload CSV #### CSV format guidelines [#csv-format-guidelines] * Required column: `phone_number` (with country code, e.g., 1, 91) * Optional columns: `name`, `company`, `reason_for_call`, etc. * Column names should match variables in your agent's context or welcome message * Additional columns are automatically passed as context to the AI agent * Maximum file size: 10 MB * Supported format: `.csv` files only * Download the provided template and add the required data columns to ensure optimal agent performance ### Campaign settings and scheduling [#campaign-settings-and-scheduling] Configure when to start your campaign and set up advanced features like auto-retry and call rescheduling. * Choose when to start: **Start Immediately** or **Schedule for Later** * If scheduling, set the date, time, and timezone * Configure Auto Retry settings for failed calls * Enable Call Rescheduling to automatically handle callback requests * Click **Next** to proceed to Step 4 Step 3: Campaign settings #### Configuration options [#configuration-options] Detailed configuration #### Auto retry settings [#auto-retry-settings] * **Maximum Retry Attempts**: Set retry limit (1–6 attempts maximum) * **Retry Schedule Immediately**: Retry failed calls right after the main campaign completes * **Retry Schedule Next Day**: Automatically retry all failed calls the following day * **Retry Schedule Scheduled Time**: Set custom days and hours delay for retry attempts * Auto retry only triggers after the main campaign completes * You can edit retry settings even after campaign creation #### Call rescheduling [#call-rescheduling] * Enable **Call Rescheduling** in campaign settings * AI automatically detects customer requests like "call me tomorrow" or "I'm busy right now" * Automatically schedules follow-up calls based on customer preferences * Works by analyzing conversation for reschedule requests during the call * Improves customer experience and conversion rates ### Review and create campaign [#review-and-create-campaign] Final review of all campaign settings before creation. * Review all campaign details: name, phone number, and agent configuration * Check the number of contacts to be called * Verify scheduling settings (immediate start or scheduled time) * Confirm auto-retry settings and maximum retry attempts * Check estimated cost and ensure sufficient account balance * Click **Create Campaign** to launch or schedule your bulk call campaign Step 4: Review and create ## Campaign management after creation [#campaign-management-after-creation] Once your campaign is created, you can monitor, control, and analyze its performance. * **Pause Campaign**: Temporarily stop an active campaign * **Resume Campaign**: Continue a paused campaign * **Update Reschedule Settings**: Enable / disable call rescheduling during campaign * **Update Auto Retry Settings**: Modify retry attempts and schedule Campaign management interface ## Best practices for successful campaigns [#best-practices-for-successful-campaigns] * Test with a small batch first before launching to your entire list * Use descriptive campaign names: include date, purpose, and target audience for easy tracking * Include relevant context columns that match your agent's variables for personalized conversations * Set appropriate retry limits: 2–3 retry attempts are usually optimal * Enable call rescheduling to improve success rates and brand reputation * Check progress regularly and adjust settings based on real-time performance * Use campaign analytics to improve future campaigns and agent performance See [Bulk outbound best practices](/docs/bulk-calls/best-practices) for the full optimization playbook. # Agent versioning and publishing (/docs/dashboard-guides/agent-versioning-and-publishing) > Save named versions of an agent, publish one as what your live traffic runs, and roll back anytime while your draft stays editable. Versioning lets you keep working on an agent without changing what your live agent does. You save named snapshots of the whole setup, publish the one you want live, and keep editing safely in a draft that only reaches traffic when you choose. If you have never touched any of this, nothing changes. Every agent starts in live-edits mode, where each change you make is live on the next call or message. Versioning and publishing are there when you want a safety net. Saving, comparing and restoring versions is included on every plan. Publishing, which keeps live traffic on one version while you keep editing, is available on Early deployers and above. Without it your agent stays in live-edits mode, and you still keep the full history. ## How changes reach live traffic [#how-changes-reach-live-traffic] There are three things to keep straight: your **draft**, your **saved versions**, and the **published version**. * **Your draft** is the agent as you are editing it right now. In live-edits mode, the draft is also what live traffic gets. * A **saved version** is a snapshot you keep. You can save as many as you like and restore any of them later. * The **published version** is the one locked in for live traffic. Once you publish, your edits stay in the draft and only reach live traffic the next time you publish. Testing always runs your draft, so you can try changes with a chat, web call, or phone call before anyone else hears them. Your draft every edit lands here Published version locked Live traffic everyone your agent talks to phone · campaigns · WhatsApp SMS · email · web widget Saved versions v1 v2 v3 Test calls chat · web call · phone call Publish serves Save Restore reads your draft You control all of this from **Version history** in the top row of the agent editor. The chip shows the version your draft is on, and turns amber when your draft has moved ahead of what callers get. Everything else, saving, restoring, and publishing, happens inside the panel it opens. The agent editor tab row with the Version history chip showing v3 ## Save a version [#save-a-version] A version is a snapshot of the entire agent setup at one moment: the prompt and its sections, voice, model, transcriber, transfer rules, post-call actions, knowledge base, and integrations. Open **Version history** from the top of the editor, name the snapshot in the form at the bottom, add an optional note, and save. The Version history panel showing a draft with unpublished changes above two saved versions, one carrying a Published badge * **Version numbers** (v1, v2, v3, and so on) never change and are how you refer to a snapshot. **Names** carry the story: your own names, plus automatic ones like "Saved at publish" and "Automatic backup". * The top of the panel always tells you what callers get right now, and which version your draft is on. * **Search** by name or number to find one fast, for example type `v4`. * **Filters** appear when they would actually narrow the list, so you never get a row of pills that all select the same versions. * **Rename or delete** any version you no longer need. The published version cannot be deleted while it is live, so publish another version or unpublish first. Saved versions are kept for about 90 days and then removed automatically, so your history stays tidy. Restoring a version resets its clock, and the version that is currently published is never removed. Each version shows when it expires, with a warning as the date gets close. ## See exactly what changed [#see-exactly-what-changed] Expand any version and open **What changed in this version** for a plain summary of what moved. Expand a change to read the edit itself, with removed words struck through and added words highlighted in place, so you can see the wording change at a glance. An expanded version showing the welcome message edit with removed words struck through and added words highlighted, plus a Restore to draft button ## Publish a version [#publish-a-version] Publishing locks a version as what your live traffic runs: phone numbers, campaigns, WhatsApp, SMS, email, and the web widget. Open **Version history**, then use **Publish** at the top of the panel and confirm. If your plan does not include publishing, that control reads **Upgrade to publish**. Everything else on this page keeps working, and every edit you make stays live as you make it. The publish confirmation dialog reading Publish these changes, explaining the draft becomes the live version for campaigns and attached numbers Once published, the top of the panel names the version callers are on, with a link to view it. The top of the panel reading Live v3, Callers are on this version, with a View published v3 link After that, your edits stay in the draft. The **Version history** chip turns amber and the panel spells out which version callers stay on until you publish again, so you always know your draft is ahead of what live traffic gets. The tab row with the Version history chip tinted amber to show the draft has unpublished changes A few things worth knowing about how a publish takes effect: * **Calls already in progress finish as they started.** Publishing never interrupts a live call. * **New calls, the remaining calls of a running campaign, and the next message in a thread use the newly published version.** * **Testing still runs your draft**, so a test call after publishing exercises your latest edits, not the published version. * You can **publish the current draft** or **publish any older saved version** straight from history. Publishing an older version does not touch your draft. * When you start a bulk campaign, the review step shows **which version the campaign will run**, so you can confirm before it goes out. ## View the published version [#view-the-published-version] To check what live traffic is getting without leaving your draft, use **View published**. The agent opens read-only with a banner across the top, and your draft is untouched while you look. Choose **Back to draft** to return to editing. A read-only banner reading Viewing published version. Fields are read-only, with a Back to draft button ## Restore a version [#restore-a-version] Restoring brings a saved version back into your draft so you can keep editing from there. Restoring never deletes anything: an automatic backup of your current setup is taken when needed, so a restore can always be undone from version history. Restoring changes your draft only. It does not change live traffic until you publish. Restoring also resets that version's expiry, so a version you keep coming back to stays available. ## Common workflows [#common-workflows] ### Iterate safely while a campaign is running [#iterate-safely-while-a-campaign-is-running] 1. Publish the script you trust, so your running campaign keeps using it. 2. Rework the prompt in your draft. The campaign is unaffected. 3. Test your draft with a chat or a test call until it sounds right. 4. Publish when ready. The remaining calls pick up the new version. ### Keep seasonal or promo scripts ready [#keep-seasonal-or-promo-scripts-ready] 1. Save your everyday script as a version named, for example, "Standard". 2. Save your campaign script as "Holiday promo". 3. Publish whichever one applies today. Swap them by publishing the other. ### Recover from a bad edit [#recover-from-a-bad-edit] 1. Open **Version history** and find yesterday's version. 2. Restore it into your draft, or use **View published** first to compare what is live against what you have now. 3. Publish once you are happy. ### Roll back live behavior instantly [#roll-back-live-behavior-instantly] 1. Open **Version history** and find the previous good version. 2. Publish it straight from history. Live traffic moves back immediately and your draft stays exactly as it was. ### Review before go-live [#review-before-go-live] 1. A teammate opens **View published** to read the current live agent, read-only. 2. You keep editing your draft at the same time. 3. Publish when the review is done. ## FAQ [#faq] No. Calls in progress finish as they started. New calls, the remaining calls of a running campaign, and the next message in a thread use the newly published version. Test calls always run your draft: chat, web call, and phone call all use your latest edits, so you can try changes before publishing them. Nothing changes. Your agent stays in live-edits mode and every change is live on the next call or message, exactly as before. Not while it is live. Publish another version or unpublish first, then delete the one you no longer need. The bulk campaign review step shows the version the campaign will run, so you can confirm it before starting. Yes. A version you save is kept for about 90 days and then removed automatically, so your history does not grow forever. Restoring a version resets its clock, and the version you have published is never removed. Each version in the list shows when it expires, and warns you as the date gets close. # Call quality and debugging (/docs/dashboard-guides/call-quality) > Understand the per-call quality score, replay a call with its event timeline, and read the latency profile. Every call has a detail view with tools to judge quality and debug what happened. Open any call from [Call Logs](https://omnidim.io/call-logs), Omni CRM, a bulk call, or an agent's recent calls, and use the tabs described below. ## Call Quality Score (CQS) [#call-quality-score-cqs] CQS is an automatic score from 0 to 5 that rates how well the agent handled a conversation. It is shown in the call detail's evaluations tab and as a column in Call Logs and Omni CRM. The score combines four dimensions: * **Intent**: did the agent stay on domain, read the caller's intent, and handle the input * **Relevance**: did each reply address the caller, stay factual, and move the conversation forward * **Coherence**: logical flow, clarity, and no needless repetition * **Latency**: how quickly the agent responded each turn A short label accompanies the number (Excellent, Good, Average, or Poor). CQS runs automatically after each call only when it is enabled for your account. You can also recompute it for a single call with the **Regenerate CQS** button in the call detail. ## Replay [#replay] The **Replay** tab plays the recording while the call's events appear in sync with the audio, turn by turn. It is the fastest way to debug a call: scrub the audio or click any event to jump to that moment. * See the conversation as bubbles, including turns that were cut off by the caller interrupting (barge-in), and setup events from before the recording started. * Expand any event to view its full details. * If there is no recording, a silent replay plays the timeline on its own clock. Call traces used for replay are retained for 90 days. ## Latency profile [#latency-profile] The **Latency Profile** tab breaks down how long each step of the voice pipeline took, so you can find the slow step before changing anything. * Summary tiles and statistics for LLM and text-to-speech latency, including averages and percentiles * A per-turn table and charts showing latency for each interaction * Per-turn speech-to-text (ASR) timing is shown in the interaction detail for phone and web calls Use it to pinpoint whether a slow call is coming from the model, the voice, or transcription. ## Video walkthrough [#video-walkthrough] # Configure your agent (/docs/dashboard-guides/configure-your-agent) > Configure the conversational flow for your agent. Sections, instructions, welcome message, and best practices. The Conversational Flow section allows you to create structured instructions for your agent, defining how it should handle different parts of a conversation and what actions it should take. With it you can: * Guide your agent through complex conversation scenarios * Ensure consistent handling of specific topics * Create a logical flow for multi-step processes * Provide detailed instructions for specialized tasks Overview of conversational flow ### Section management [#section-management] Manage different stages of the conversation by creating, reordering, or removing instruction sections. * **Add sections**: Create new instruction blocks for different conversation stages * **Reorder sections**: Drag and drop to change the sequence of instructions * **Remove sections**: Delete unnecessary instruction blocks Section management ### Section configuration [#section-configuration] Customize the content and behavior of each section in the conversational flow. * **Section title**: A descriptive name for this conversation stage * **Instructions**: Detailed guidance for how the agent should handle this part of the conversation Section configuration ### Welcome message [#welcome-message] The welcome message is the first thing the caller hears. Add or edit it from the conversation flow section. * **Static (recommended)** plays the message exactly as written. It is the fastest and most consistent, so keep the dynamic toggle off for most agents. * **Dynamic** generates the opening each call from a short prompt. * **Personalize** with variables in square brackets, like `[name]` or `[city]`, filled from your contact list at call time. Variable names must match your contact list column headers. To mask any startup delay, enable Initial Ringing Sound under Call Configuration > Ambient Sound. Welcome message ### Plan your conversation structure [#plan-your-conversation-structure] Before adding sections, outline the key stages of your ideal conversation, such as Introduction, Information gathering, Problem solving, and Conclusion. ### Create sections for each stage [#create-sections-for-each-stage] Add a section for each conversation stage with clear, descriptive titles and detailed instructions for the agent. ### Provide detailed instructions [#provide-detailed-instructions] In each section, include what information to collect, how to respond to specific questions, when to move to the next stage, and any actions to take (e.g., scheduling or data collection). ### Test and refine [#test-and-refine] After setting up your flow, test conversations to ensure the agent follows instructions. Refine sections based on user interactions and add new ones as needed. ## Best practices [#best-practices] * Write instructions in clear, direct language * Include example responses for complex scenarios * Specify exactly what information to collect * Provide fallback instructions for unexpected situations * Keep sections focused on specific conversation stages ## Pro tips [#pro-tips] * Start with fewer sections and add more as needed * Use the drag-and-drop feature to optimize conversation flow * Review conversation recordings to identify areas for improvement * Update your flow regularly based on common user interactions # Conversation Insights (/docs/dashboard-guides/conversation-insights) > Bulk-audit call recordings from OmniDimension or external sources and get aggregate quality insights. Conversation Insights (shown in-product as **Call Recording Analysis**) audits batches of call recordings. You create an analysis, point it at a source of recordings, and the system transcribes and scores every call, then shows aggregate insights across the whole batch. Conversation Insights may need to be enabled for your account. Audits are billed per audio minute, with a free allowance shown in the dashboard. ## Where to find it [#where-to-find-it] In the dashboard sidebar, open **Conversation Insights**. ## Sources you can audit [#sources-you-can-audit] You can pull recordings from OmniDimension Voice AI as well as external sources, including Google Drive, direct upload, Tata Tele, Ozonetel, LeadSquared, Genesys Cloud, Five9, and Uniphore. ## One-off and scheduled audits [#one-off-and-scheduled-audits] * **Audit**: a one-time analysis of a batch of recordings. * **Scheduled audit**: runs continuously, pulling new recordings from the source on a set interval within a time window. ## What you get [#what-you-get] Each analysis opens a dashboard with: * Overview metrics: total calls, total and average duration, average sentiment, and compliance * A performance breakdown across compliance, operational efficiency, agent behavior, resolution, and customer experience * A score distribution you can drill into to find low-scoring calls * An agent leaderboard and per-agent performance * Recurring failure patterns and voicemail analysis * Per-call transcripts and an audit assistant you can ask about the results Use it to see where agents fall short across a whole batch: compliance gaps, recurring problems, per-agent quality, and the specific calls worth reviewing. ## Walkthrough: run your first audit [#walkthrough-run-your-first-audit] ### Create an analysis [#create-an-analysis] Open Conversation Insights and create a new analysis. Give it a name. ### Choose a source [#choose-a-source] Pick where the recordings come from: OmniDimension Voice AI, a direct upload, Google Drive, or a connected provider (Tata Tele, Ozonetel, LeadSquared, Genesys Cloud, Five9, Uniphore). ### Run once or on a schedule [#run-once-or-on-a-schedule] Run a one-time audit, or set up a scheduled audit that pulls new recordings on an interval within a time window. ### Review the results [#review-the-results] Open the analysis dashboard: overview metrics, the performance breakdown, the score distribution (drill into the low-scoring range), the agent leaderboard, and recurring failure patterns. Use the audit assistant to ask questions about the results. ### Act on it [#act-on-it] Pull the low-scoring calls, listen to them, and fix the agent prompt or flow based on the patterns you see. ## Video walkthrough [#video-walkthrough] # Dashboard guides (/docs/dashboard-guides) > No-code walkthroughs for configuring agents, numbers, knowledge base, and post-call delivery from the OmniDimension dashboard. These guides walk through everything you can do from the OmniDimension dashboard without writing a line of code, from building your first agent's conversational flow to wiring up post-call delivery to Salesforce and Slack. - [Configure your agent](/docs/dashboard-guides/configure-your-agent) — Sections, instructions, welcome message, and best practices for conversational flow. - [Agent versioning and publishing](/docs/dashboard-guides/agent-versioning-and-publishing) — Save named versions, publish one as what your live traffic runs, and roll back anytime. - [Voices and languages](/docs/dashboard-guides/voices-and-languages) — 1000+ voices, 90+ languages, persona categories, and multilingual support. - [Voice cloning](/docs/dashboard-guides/voice-cloning) — Clone your voice and assign it to any agent for a personalized experience. - [Knowledge base](/docs/dashboard-guides/knowledge-base) — Provide domain-specific knowledge to your agent through document references. - [Numbers Shop](/docs/dashboard-guides/numbers-shop) — Purchase phone numbers in India and the US, including the eKYC verification flow. - [Web chat widget](/docs/dashboard-guides/web-chat-widget) — Deploy your agent as a chat widget on any website with a copy-paste embed script. - [Web search tool](/docs/dashboard-guides/web-search-tool) — Enable real-time internet access for your agent. - [Recent calls](/docs/dashboard-guides/recent-calls) — Access and analyze recent call logs to improve your agent's performance. - [Post-call actions](/docs/dashboard-guides/post-call) — Deliver structured post-call data to Email, Salesforce, Slack, Webhooks, HubSpot, and WhatsApp Cloud. - [Agent timezone](/docs/dashboard-guides/update-timezone) — Set and update your agent's timezone for accurate scheduling and time-based operations. # Knowledge base (/docs/dashboard-guides/knowledge-base) > Provide your agent with domain-specific knowledge using document-based references. Knowledge Base allows your agent to access and utilize external documents and files, enabling it to reference specific information during conversations with users. With it you can: * Provide your agent with domain-specific knowledge * Upload documents for reference during conversations * Ensure accurate and consistent information delivery * Reduce the need for manual information updates ### Setup option 1: from agent configuration [#setup-option-1-from-agent-configuration] * Go to your agent's edit page * Select the **Knowledge Base** tab * Upload files or select from your existing library * Files will be automatically processed and made available to your agent Upload or select files from agent editor ### Setup option 2: from the main dashboard [#setup-option-2-from-the-main-dashboard] * Go to the **Files** page in your dashboard * Upload files or select from your existing library * Go to your agent's edit page * Select the **Knowledge Base** tab * Attach files for the agent Dashboard file upload Link uploaded files to agent ### Configuration guide: uploading files [#configuration-guide-uploading-files] * Click the **Upload File** button in the Knowledge Base section * Select documents from your computer (supported formats: PDF) * Wait for processing to complete * Newly uploaded files will appear in your file library ### Using with your agent [#using-with-your-agent] * The agent will automatically reference these documents when answering user queries * Information from these files will be used to provide accurate and relevant responses * The agent will cite specific documents when providing information from them ### Pro tips [#pro-tips] * Upload comprehensive documentation for best results * Use descriptive filenames to easily identify content * Organize information in clear, structured documents * Regularly update your knowledge base with the latest information * Attach only relevant files to each agent to maintain focus **Video tutorial: Adding Knowledge Base.** Watch this guide on how to add and manage knowledge base documents for your agent on [YouTube](https://www.youtube.com/watch?v=7FJoWB4UqRg). # Live call monitoring (/docs/dashboard-guides/live-call-monitoring) > Watch in-progress calls as live transcripts and status, updated in near real time. Live call monitoring shows the calls happening right now, as live transcripts and status. Use it to keep an eye on active conversations and see how each call is going before it ends. This is transcript and status monitoring, not audio listen-in. It shows the live conversation as text, refreshed every 30 seconds. It may need to be enabled for your account. ## Where to find it [#where-to-find-it] In the dashboard sidebar, open **Monitor Live Call**. ## What it shows [#what-it-shows] * Every in-progress call with its live status (for example connecting, AI speaking, transferred, or ended) * A live transcript of each call, turn by turn * A call summary (customer name, context, and next steps) once the call is transferred to a human or ends ## What you can do [#what-you-can-do] * Watch conversations unfold in near real time * Filter and search by agent, number, or status * Read the AI-generated summary when a call finishes or is handed off ## Video walkthrough [#video-walkthrough] # Numbers Shop (/docs/dashboard-guides/numbers-shop) > Purchase dedicated phone numbers and connect them to your Voice AI agents. Purchase dedicated phone numbers and connect them to your Voice AI agents. This is the fastest way to get a number: no external provider account and no setup. If you'd rather bring a number you already own, see [Get a phone number](/docs/get-a-number) for all your options. ## Available numbers [#available-numbers] Buy a number in: * 🇮🇳 **India (+91)** * 🇺🇸 **United States (+1)** Your number can make calls to 70+ countries. ## Pricing [#pricing] ### Monthly rental [#monthly-rental] Each number has a monthly fee shown when you browse. You pay for 30 days at a time and it auto-renews. ### Telephony channels [#telephony-channels] Channels are how many calls can run at the same time. Concurrency applies to your whole account, not a single number, so every agent and number draws from the same shared pool. * Your plan includes a base number of channels. * Add more anytime from [Billing → Telephony](https://omnidim.io/billing?tab=numbers) at $6.74 per channel per month, billed from your wallet. Release a channel whenever you no longer need it. * Sizing: channels = (calls per hour x average call minutes) / 60. Round up and add a buffer. See [Plans and pricing](/docs/pricing) for the full breakdown. ### Payment [#payment] Purchases are deducted from your wallet balance. [Top up your wallet here](https://omnidim.io/billing). ## How to purchase [#how-to-purchase] ### India (+91) [#india-91] **Quick one-time verification.** Indian regulations require eKYC verification before your first purchase. It takes under 2 minutes, and after that all purchases are instant. Go to **Numbers Shop** in your dashboard and select an Indian number. Click **Reserve & Start KYC**. The number is held while you verify. Complete the 6-step eKYC process. See [eKYC Verification](#ekyc-verification-india) below. Confirm purchase. Attach the number to your Voice AI agent from the **Phone Numbers** page. ### United States (+1) [#united-states-1] Go to **Numbers Shop** in your dashboard and search for a US number. Click **Purchase** and confirm. Attach the number to your Voice AI agent from the **Phone Numbers** page. ## eKYC verification (India) [#ekyc-verification-india] eKYC is a quick, one-time identity verification required by Indian telecom regulations. It takes under 2 minutes to complete, and after that you can purchase unlimited +91 numbers instantly. ### Documents required [#documents-required] * **PAN Card**: Your 10-character PAN number * **Aadhaar Card**: Must be linked to a mobile number for OTP * **GST Number** (optional): Only if you're GST registered ### Verification steps [#verification-steps] **Registration**: Enter your name, email, and phone. Verify with OTP. **PAN verification**: Enter your PAN number and select business type. **Aadhaar OTP**: Enter your Aadhaar. OTP will be sent to your linked mobile. **Aadhaar verification**: Enter the OTP to verify your identity. **GST verification**: Enter your GST number if applicable, or skip this step. **Review and submit**: Check your details and complete the verification. **Done in under 2 minutes.** Complete eKYC once and buy unlimited +91 numbers instantly, anytime. ## Managing your numbers [#managing-your-numbers] After purchasing, manage your numbers from the **Phone Numbers** page. * **Attach to agent**: Connect your number to a Voice AI agent to enable calls. * **Renewals**: Numbers auto-renew monthly if your wallet has sufficient balance. * **Release**: Release numbers you no longer need. They won't renew next month. ## Get started [#get-started] Ready to get your phone numbers? * **Browse Numbers Shop**: Find and purchase phone numbers * **My Phone Numbers**: Manage your existing numbers * **Top Up Wallet**: Add funds for purchases Already have your own phone infrastructure? Use [SIP Trunking](/docs/telephony) to connect your existing numbers to OmniDimension. # Omni CRM (/docs/dashboard-guides/omni-crm) > A single place to search, filter, and analyze every customer interaction your agents have handled. Omni CRM gives you one searchable table of every interaction your agents have handled, with one row per call. Use it to review outcomes, listen to recordings, read summaries and transcripts, and export the data. Omni CRM may need to be enabled for your account. If you do not see it in the sidebar, contact support. ## Where to find it [#where-to-find-it] In the dashboard sidebar, open **Omni CRM**. ## What each row shows [#what-each-row-shows] * Contact name, and the agent (bot) that handled the interaction * To and from number, date, and duration * Outcome (for example completed or voicemail) and sentiment * Call Quality Score (see [Call quality](/docs/dashboard-guides/call-quality)) * Recording with an inline player and download * A button to open the full call log, summary, and transcript Any post-call variables your agent extracted are added automatically as extra columns, so you can scan captured fields at a glance. ## What you can do [#what-you-can-do] * Filter by agent, outcome, sentiment, or date * Search across interactions * Listen to recordings and read summaries and transcripts * Export the filtered view to CSV ## Walkthrough: review and export calls [#walkthrough-review-and-export-calls] ### Filter to what you need [#filter-to-what-you-need] Filter by agent, outcome, sentiment, or date, or search across interactions. ### Dig into a call [#dig-into-a-call] Open a row to play the recording, read the summary and transcript, and check the extracted variables and Call Quality Score. ### Export [#export] Export the filtered view to CSV to share or analyze elsewhere. Omni CRM is a place to review and analyze interaction data. To manage a pipeline of leads, use the **Leads** page instead. ## Video walkthrough [#video-walkthrough] # Post-call actions (/docs/dashboard-guides/post-call) > Configure post-call delivery to Email, Salesforce, Slack, Webhooks, HubSpot, and WhatsApp Cloud. After each call ends, OmniDimension allows you to deliver structured post-call data to external destinations: * Email * Salesforce * Slack * Webhook * HubSpot * WhatsApp Cloud **Video tutorial: Get Post-Call Data.** Learn how to retrieve and manage post-call data from your Voice AI agent on [YouTube](https://www.youtube.com/watch?v=__hZ9u6Em9w). ## How to access post-call delivery settings [#how-to-access-post-call-delivery-settings] * Go to your Agent Dashboard on OmniDimension. * Navigate to the **Post-Call** section. * Under the **Delivery Method** dropdown, you will see options for Email, Salesforce, Slack, Webhook, HubSpot, and WhatsApp Cloud. Post-Call Delivery Guide ## Email delivery [#email-delivery] Use this method to receive an email with the full call report. * Select **Email** from the dropdown. * Enter the recipient email address where you'd like the report to be sent. * Configure the include fields like summary, sentiment, variables, full conversation, etc. Email delivery configuration ## Salesforce integration [#salesforce-integration] Push post-call data directly to a specific object in your Salesforce CRM. * Select **Salesforce** from the dropdown. * Choose the Salesforce integration from the dropdown. If not available you can create a new integration for Salesforce from the **Integrations** tab. * Choose the target object: Lead, Contact, Account, Product, etc. * It will automatically map the extracted variables to Salesforce fields. Salesforce integration configuration ## HubSpot integration [#hubspot-integration] Send call summaries and variable data to HubSpot CRM records. * Select **HubSpot** from the dropdown. * Choose the HubSpot integration from the dropdown. If not available you can create a new integration for HubSpot from the **Integrations** tab. * Choose the target object: Contact, Deal, Ticket, Company. * It will automatically map the extracted variables to HubSpot fields. HubSpot integration configuration ## Slack integration [#slack-integration] Post a message to a Slack channel after every call. * Choose **Slack** as the delivery method. * Choose the Slack integration from the dropdown. If not available you can create a new integration for Slack from the **Integrations** tab. * Select the channel to which messages should be posted (e.g., `#sales-leads`, `#support-alerts`). * Choose what data to include: summary, sentiment, extracted variables, full conversation, etc. Slack integration configuration ## Webhook [#webhook] Send structured JSON data to any external system or automation tool (Make, Zapier, n8n, etc.). * Select **Webhook** from the delivery method dropdown. * Paste the webhook URL of your receiving system. * Select **Standard JSON** to send our data as-is, or select **Custom Payload** to transform it. * If using **Custom Payload**: Click **Test Template**, write your logic, and verify with the Real-Time Playground. Webhook configuration ### Reference: available variables [#reference-available-variables] Use these variables in your custom payload: * `phone_number`: Customer's phone number * `to_number`: Agent / bot phone number * `bot_name`: Name of your agent * `call_status`: e.g., `"completed"` * `call_report.summary`: AI summary of the call * `call_report.sentiment`: `"Positive"`, `"Neutral"`, or `"Negative"` * `call_report.extracted_variables.[key]`: Your custom extracted data (e.g., `$.call_report.extracted_variables.email`) ### Example: LeadSquared schema [#example-leadsquared-schema] ```json [ { "Attribute": "Phone", "Value": "phone_number" }, { "Attribute": "Name", "Value": "call_report.extracted_variables.name" }, { "Attribute": "Email", "Value": "call_report.extracted_variables.email" }, { "Attribute": "Notes", "Value": "call_report.summary" } ] ``` **Pro tip: real-time testing.** Before testing your template, make sure to add your **Extracted Variables** in the configuration. The **Test Template** playground will automatically populate with realistic dummy data based on your specific variable names, allowing you to verify your custom payload logic instantly. ## WhatsApp Cloud post-call [#whatsapp-cloud-post-call] Send a WhatsApp message to the user with the call summary or other details using a template. * Select **WhatsApp Cloud** as the delivery method. * Choose an available WhatsApp Cloud number from the dropdown. * Select one of your pre-approved WhatsApp templates. * Map the template variables to your extracted variables (e.g., `{{1}}` = `$.call_report.extracted_variables.name`). * Add a description for this post-call action for your reference. ## Best practices [#best-practices] * Test each delivery method before going live using the built-in **Test Connection** feature. * For Slack, Salesforce, and HubSpot, ensure you grant the necessary permissions during OAuth. * Use Webhook.site or RequestBin to inspect real payloads during integration. * Always sanitize or encrypt sensitive data when sending via webhook or email. # Recent calls (/docs/dashboard-guides/recent-calls) > Access, analyze, and make use of recent call logs to improve your agent's performance. Recent Calls provides a comprehensive history of all interactions between your agent and users, allowing you to review past conversations and analyze performance. With it you can: * Track all agent interactions in one place * Review conversation transcripts * Identify potential issues or improvement areas * Monitor agent performance over time * Access detailed call analytics ### Accessing recent calls from the agent dashboard [#accessing-recent-calls-from-the-agent-dashboard] * Go to your agent's management page * Select the "Recent Calls" tab * View the chronological list of all interactions for that specific agent Recent Calls view from the Agent Dashboard ### Accessing recent calls from the main dashboard [#accessing-recent-calls-from-the-main-dashboard] * Go to the Call Logs page in your dashboard * View the chronological list of all interactions across all your agents Main dashboard showing all agent logs Detailed log analytics and filters ### Features [#features] * Caller phone number or identifier * Date and time of call * Call duration * Call type * Status indicators (Issues / No Issues) * Complete conversation transcript * Download recording ### Using recent calls [#using-recent-calls] * Monitor your agent's performance * Identify common user questions or issues * Improve your agent's responses based on real conversations * Track usage patterns over time **Video tutorial: Review Call Analytics.** Learn how to review recordings, transcripts, and performance metrics on [YouTube](https://www.youtube.com/watch?v=9GeMHYBm5js). # Agent timezone (/docs/dashboard-guides/update-timezone) > Set and update your agent's timezone for accurate scheduling and time-based operations. Setting the correct timezone for your agent is crucial for accurate scheduling, appointment booking, and time-based operations. This ensures your agent operates according to your local business hours and can schedule meetings correctly. ### Why timezone matters [#why-timezone-matters] * Accurate appointment scheduling with calendar integrations * Proper business hours operation * Correct time display in conversations * Synchronized meeting invitations and confirmations ### Method 1: Update via Settings [#method-1-update-via-settings] The most direct way to update your agent's timezone through the settings page. * Navigate to your dashboard and go to the Settings page * Find the Timezone section and choose your desired timezone from the dropdown menu * Your agent will now operate in the selected timezone Go to Settings page Update Timezone ### Method 2: Automatic timezone notification [#method-2-automatic-timezone-notification] When you add calendar integrations, the system automatically prompts you to update the timezone. * Add a Google Calendar or Cal.com integration to your agent * A notification will automatically appear asking to update timezone * Click on the "Update Timezone" notification * From the timezone modal, select the appropriate timezone and save it Timezone Notification Update Timezone Modal ### Method 3: Over the API [#method-3-over-the-api] Set a timezone per agent with the `timezone` field on [create agent](/docs/api-reference/agents/createAgent) or [update agent](/docs/api-reference/agents/updateAgent), using an IANA timezone name. Useful when you run agents for clients in different regions. ```bash curl -X PUT https://backend.omnidim.io/api/v1/agents/{agent_id} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"timezone": "Asia/Kolkata"}' ``` An agent without its own timezone falls back to the account timezone from Settings. # Voice cloning (/docs/dashboard-guides/voice-cloning) > Clone your voice and use it with your AI agents for a personalized experience. Voice cloning lets you create a digital replica of your own voice. Once created, you can assign it to any of your AI agents so that they speak in your voice during live calls. This makes your agents sound more natural and personal, improving the listener's experience. ### Navigate to Cloned Voices [#navigate-to-cloned-voices] From the main dashboard, open the sidebar and click on **Cloned Voices**. This is your central hub for managing all voice clones. The Cloned Voices page in the sidebar ### Upload your audio sample [#upload-your-audio-sample] Click the button to create a new cloned voice. Fill in the form with a name for your voice, select the language, and upload an audio sample of yourself speaking. For best results, use a clear recording of at least 10 seconds with minimal background noise. * Give your cloned voice a recognizable name * Select the language that matches your audio sample * Upload an MP3, WAV, or WebM audio file (minimum 20 seconds) Fill in the name, language, and upload your audio sample ### Voice created successfully [#voice-created-successfully] Once the upload is processed, your cloned voice will appear in the list with a **Ready** status. It is now available to assign to any of your agents. Your cloned voice is ready and available for use ### Use your cloned voice in an agent [#use-your-cloned-voice-in-an-agent] Open any agent's settings and go to the Voice tab. Switch to the **Cloned Voices** tab to see your available voice clones. Select the voice you just created and save the agent configuration. The agent will now use your cloned voice on all future calls. * Open an agent and navigate to the Voice configuration tab * Click on the **Cloned Voices** tab * Select your cloned voice from the list * Save the agent to apply the change Choose your cloned voice from the Cloned Voices tab in the agent voice picker # Voices and languages (/docs/dashboard-guides/voices-and-languages) > Build globally-aware AI agents with native-level fluency across 90+ languages and 1000+ realistic voices. Build globally-aware AI agents. Our platform provides native-level fluency across 90+ languages with highly emotive voice generation. * **90+ Languages** * **1000+ Realistic Voices** * **Sub-200ms Latency** ## Core capabilities [#core-capabilities] Our platform combines Speech-to-Text (STT) and Text-to-Speech (TTS) so your agent can hear and speak naturally. - **Language Understanding (STT)** — Your agent hears and transcribes words with precision. We use high-fidelity models that excel at recognizing intent across diverse regional accents. Real-time transcription, dialect and accent support, automatic language detection, telephony-optimized models. - **Speech Generation (TTS)** — Your agent speaks using realistic, brand-aligned voices. Access a library of 1000+ personas designed for everything from support to storytelling. Emotive and natural pacing, 1000+ voice library, multilingual v2 support. ## Recommended settings [#recommended-settings] Starting points that work well for most agents. Tune from here after you test with a real script. * **Text-to-speech (TTS)**: Cartesia is a good default for low latency and natural voices. To speak in your own brand voice, see [Voice cloning](/docs/dashboard-guides/voice-cloning). * **Speech-to-text (STT)**: Soniox for Indian and global languages, or Sarvam for Indian languages. Turn on the noise reducer, and set the silence timeout to around 200 ms (how long the agent waits after the caller stops speaking). * **LLM**: GPT-4.1 Mini at temperature 0.35 with streaming on. Streaming lets the agent start speaking while the reply is still generating, which lowers latency. * **Minimum words to interrupt**: default is 2. Set it to 3 so short words like "yeah" or "okay" do not cut the agent off. Enable only the languages the agent needs. Extra languages add processing. Need one agent to call different people in different languages? You can set the languages per call at dial time instead of on the agent. See [Per-call languages](/docs/per-call-languages). ## Global support matrix [#global-support-matrix] Comprehensive coverage for both understanding and speaking across 90+ locales. ### Indian languages [#indian-languages] Hindi Bengali Telugu Marathi Tamil Urdu Gujarati Kannada Malayalam Punjabi Assamese Maithili Odia Sanskrit ### Global languages [#global-languages] English Spanish French German Japanese Chinese Portuguese Italian Dutch Korean Arabic Turkish Russian Vietnamese Indonesian Swedish Polish Thai Greek Hebrew Czech Danish Finnish Norwegian Romanian Ukrainian Hungarian Slovak Croatian Bulgarian Lithuanian Latvian Estonian Slovenian Serbian Macedonian Albanian Bosnian Montenegrin Maltese Icelandic Welsh Irish Scots Gaelic Basque Catalan Galician Filipino Malay Khmer Lao Burmese Nepali Sinhala Bengali (Bangladesh) Tamil (Sri Lanka) Swahili Amharic Zulu Xhosa Afrikaans Somali Hausa Yoruba Igbo Farsi Pashto Kurdish Azerbaijani Armenian Georgian Kazakh Uzbek Turkmen Kyrgyz Tajik Mongolian \+ many more **Multi-lingual switching.** Your agent can automatically detect the language being spoken and respond in kind, so the conversation flows in whatever language the caller uses. # Web chat widget (/docs/dashboard-guides/web-chat-widget) > Deploy your voice AI agent as a chat widget on your website with a no-code embed script. The Web Bot Widget allows you to deploy your OmniDimension agent on any website as a floating chat interface. Customers can engage with your AI agent directly from your site. With it you can: * Deploy with a simple copy-paste script, no code required * Customize design: logo, colors, position, and dimensions * Run on all modern websites * Preview the widget live before deployment ### How to deploy the widget [#how-to-deploy-the-widget] * Go to your agent dashboard * Click the **Deploy** dropdown * Select **Web Bot Widget** * Copy the generated embed script * Paste this script inside the `` tag of your website's HTML page * Save and deploy your website * The widget will appear on your website * Users can interact with your agent by clicking on the widget icon Click on Web Bot Widget from Deploy menu Copy the widget script from the deployment panel Paste script into your site before closing body tag Web widget demo ### Widget configuration [#widget-configuration] After clicking **Web Bot Widget** from the Deploy menu, you'll be taken to the configuration screen where you can customize the widget's appearance. * **Widget title**: Label displayed on the widget (e.g., Product Support) * **Logo URL**: URL to a 32x32 image or SVG icon * **Widget position**: Choose between bottom right or left * **Widget width and height**: Adjust the size in pixels * **Colors**: Background and text color for branding Edit title, logo, position, and more ### Preview and test [#preview-and-test] * View the live preview panel on the right to verify widget appearance * Use Chat or Web Call options to simulate user interaction * Adjust any settings until you're satisfied with the layout and branding ### Deployment tips [#deployment-tips] * Place the script at the bottom of the body tag for optimal loading * Ensure the widget key in the script matches your agent * Use HTTPS hosting for proper widget functionality * Test responsiveness on mobile and desktop browsers # Web search tool (/docs/dashboard-guides/web-search-tool) > Enable real-time internet access for your agent using the Web Search integration. Web Search lets your agent retrieve up-to-date information from the internet during a call, so it can answer questions about current events, facts, and data that may not be in its training data or knowledge base. ## Benefits [#benefits] * Access to real-time information * Enhanced agent responses with factual data * Ability to answer questions about current events ## Setup [#setup] ### Open the agent's integration tab [#open-the-agents-integration-tab] Navigate to your agent's page and locate the Web Search toggle in the integration tab. Enabling Web Search from the agent integration tab ### Enable Web Search [#enable-web-search] Toggle the switch to enable Web Search. Pick a search engine (OpenAI or DuckDuckGo). The agent configuration auto-saves. ## Usage tips [#usage-tips] * Web Search is most effective for factual queries that need up-to-date information. * The agent automatically decides when to invoke web search based on the query. * For time-sensitive information (news, weather, stock prices), web search returns the most accurate responses. # Healthcare appointment booking (/docs/examples/health-appointment) > Build a voice agent for healthcare appointment scheduling, doctor availability, and clinic info. Healthcare appointment booking agent overview Walk through configuring a complete appointment-booking voice agent. The agent greets patients, identifies intent, books slots via Google Calendar, answers FAQs from a knowledge base, and emails post-call summaries. ### Write the prompt [#write-the-prompt] Start the setup by writing a clear initial prompt that defines the purpose of the bot. > **Example prompt**: Create a voice AI agent for Sunrise Health Clinic > to help patients book appointments, check doctor availability, and get > basic clinic info through voice. Writing the initial prompt ### Answer clarifying questions [#answer-clarifying-questions] The platform will prompt you with clarifying questions. Answer these precisely to help tailor the voice agent for your specific use case. You can select from provided options or write your own custom answers if the options don't match your needs. Answering clarifying questions ### Automatic agent creation [#automatic-agent-creation] The system uses your prompt and answers to auto-generate an initial version of the voice assistant. This includes welcome message, basic conversational flow, collecting user data, and variable extraction in post-call. Automatic agent creation ### Review and customize agent details [#review-and-customize-agent-details] * Navigate to the **Details** section of the platform * Edit the welcome message. Ensure it is friendly and aligned with your brand voice. * Review each step of the user journey (greeting, intent identification, slot filling, confirmation) * Edit existing prompts or add new sections to better guide the conversation, edge cases, or FAQs Customizing agent details ### Test the agent [#test-the-agent] Use the platform's testing tools at the top right of the window: * **Test With Chat** for text-based interaction * **Test With Webcall** for voice-based testing Test with chat Test with web call ### Configure model and voice [#configure-model-and-voice] In the **Configuration** tab you can edit or adjust agent configuration. * **Model**: select the desired LLM * **TTS**: configure text-to-speech and choose appropriate voice settings * Adjust filler words and other behavior customization settings as needed Updating model Updating voice ### Calendar integration [#calendar-integration] Integrate with Google Calendar to enable automatic booking. * Open the **Integrations** tab * Click **Connect** next to Google Calendar * Complete the OAuth flow to securely link your account * Once connected, the calendar will be automatically attached to the agent * The bot can check availability, book slots, and send invites Integration tab view Adding Google Calendar integration Attach integration view See [Google Calendar integration](/docs/integrations/google-calendar) for the full walkthrough. ### Knowledge base [#knowledge-base] You can add a knowledge base to your bot to provide additional information. * Navigate to the **Knowledge Base** section * Upload relevant documents (for example, PDFs with clinic policies, service descriptions) * Documents are auto-attached to the bot * You can also set rules for when and why the knowledge base should be utilized Upload documents ### Post-call actions [#post-call-actions] Set up what happens after the call is completed. * Go to the **Post Call** section * Select **Email Delivery** as the post-call action * Enter the recipient's email address * Choose what to include: call summary, full transcript, sentiment analysis, variable extraction Post-call settings ### Final testing and monitoring [#final-testing-and-monitoring] * Re-test the agent via chat and web call to confirm all functionality * Validate booking flow, fallback responses, and knowledge base retrieval * Navigate to the **Call Logs** section to monitor call interactions with evaluation details Call logs # Examples (/docs/examples) > Real-world voice agent builds you can learn from and adapt. Healthcare, real estate, support, recruiting, and more. These are reference builds, not drop-in templates. Each one walks through the prompt design, knowledge base setup, integrations, and test calls behind a working agent — so you can study the moving parts, borrow what fits, and assemble your own. - [Health appointment booking](/docs/examples/health-appointment) — Schedule patient appointments with calendar integration and structured intake. - [Real estate (API)](/docs/examples/real-estate) — Property search, agent handoff, and lead routing via custom API. - [SaaS OmniSupport agent](/docs/examples/omnisupport-agent) — Inbound support agent with web-scraped knowledge, Cal.com booking, Slack alerts, and Make.com webhooks. - [Recruit bot](/docs/examples/recruit-bot) — Phone-screen candidates, qualify leads, and push to your ATS. - [Restaurant agent (API)](/docs/examples/restaurant) — Take reservations, manage waitlist, and integrate with POS via API. - [Utility bill payment (API)](/docs/examples/utility-bill-payment) — Accept payments, look up balances, and automate reminders. - [Insurance claim status](/docs/examples/insurance-claim-status) — Look up claim status, route to humans, and log to Google Sheets via Make. - [Travel planning agent](/docs/examples/travel-planning-agent) — Plan trips, suggest destinations, and capture booking intent. ## Want the code? [#want-the-code] These builds are configured in the dashboard. For runnable code you can clone, see [`Omnidim/examples`](https://github.com/Omnidim/examples) (MIT): browser calls, custom API actions, outbound campaigns, post-call webhooks, and MCP, in both TypeScript and Python. Every example runs on fixtures before you add credentials. # Insurance claim status and filing (/docs/examples/insurance-claim-status) > Build a voice agent that handles insurance claim status checks, new claim filing, and customer inquiries. Insurance claim agent overview Create a complete insurance claim voice agent on OmniDimension. The agent checks claim status, files new claims, answers policy questions from a knowledge base, and logs every call to a Google Sheet via Make.com. ### Configure your agent prompt [#configure-your-agent-prompt] Start by creating your voice agent with an insurance-specific prompt, or write a simple prompt and use the **Enhance** button to make it more descriptive. Agent prompt configuration ### Answer clarifying questions [#answer-clarifying-questions] Complete the agent setup by answering important questions about your insurance company, claim types, and verification procedures. Configuration questions ### Test your agent via web call [#test-your-agent-via-web-call] Once your agent is configured, test it through a web call to verify its functionality. * Navigate to the testing interface * Click **Test with Web Call** to initiate a test conversation * Speak with your agent to verify proper greeting and introduction * Test claim status requests and new claim filing scenarios * Verify natural conversation flow and appropriate responses to policy questions Web call testing interface ### Enhance with knowledge base PDFs [#enhance-with-knowledge-base-pdfs] Upload relevant documents to improve your agent's knowledge about insurance policies and claims processing. * Navigate to the **Knowledge Base** tab * Click **Upload PDFs** * Drag and drop or select your insurance documents * PDFs must be under 10MB in size Knowledge base configuration ### Configure post-call settings [#configure-post-call-settings] Set up data extraction for valuable insights from customer calls. * Navigate to the **Post-Call** tab * Set delivery method to **Email** and enter the recipient address * Choose data to include: Summary, Full Conversation, Sentiment, Extracted Info * Add extracted variables like caller name, policy number, claim number, incident date, and claim type Post-call settings ### Customize speech-to-text [#customize-speech-to-text] Fine-tune the speech recognition for insurance terminology. * Navigate to the **Models** section * Under **Speech-to-Text Configuration**: * Select provider (for example, Deepgram Stream) * Adjust **Silence Timeout** * Set **Silence Threshold** Speech-to-text configuration ### Customize voice and behavior [#customize-voice-and-behavior] Personalize your agent's voice and interaction style to match your insurance brand. * Go to the **Voice** section and select a provider (for example, Eleven Labs) * Browse, filter, and test voices to match your brand tone * Choose a voice with a professional, friendly, or empathetic style * Configure behavior to match your service style (Friendly and Helpful, Professional, Casual, Empathetic) Voice selection Behavior configuration ### Set up Make.com webhook integration [#set-up-makecom-webhook-integration] Enhance your agent with webhook integration to automatically log all call data in a spreadsheet. * Log in to your Make.com account and create a new scenario * Start with a webhook trigger and copy the generated URL * Add a Google Sheets module and connect your Google account * Map webhook data to the spreadsheet columns * In OmniDimension, set Webhook as the delivery method and paste the URL * Select data to include: Summary, Full Conversation, Sentiment, Extracted Info * Test the webhook to ensure call data is recorded properly Make.com webhook setup Google Sheets configuration OmniDimension webhook configuration Google sheet view with sample data See [Make, Zapier, n8n, GHL](/docs/integrations/zapier-make-n8n) and [post-call actions](/docs/dashboard-guides/post-call) for the full integration reference. ### Final testing and monitoring [#final-testing-and-monitoring] Before deploying your agent to production, conduct thorough testing and set up monitoring. * Conduct thorough testing with various claim scenarios * Validate all integrations are working properly * Check post-call delivery to ensure data is being correctly captured * Monitor initial calls and adjust configuration as needed * Review call logs and analytics to identify improvement opportunities # SaaS OmniSupport agent (/docs/examples/omnisupport-agent) > Build a voice AI agent for SaaS support with Cal.com, Slack, and Make.com integrations. OmniSupport agent overview Walk through configuring a complete inbound support voice agent for OmniDimension itself. The agent introduces itself, answers product questions from a scraped knowledge base, books demos via Cal.com, and delivers post-call summaries to Slack and Make.com. ### Write the prompt [#write-the-prompt] Start by writing a clear initial prompt that defines the purpose of the bot. ```text You are OmniSupport, an inbound customer support voice assistant for OmniDimension. Speak in a friendly, polite, and professional tone. Your main goals are to: 1. Greet the caller and introduce yourself as OmniSupport. 2. Understand the caller's intent. Whether they want to learn about the product, explore features, or book a demo. 3. Provide brief, clear responses about OmniDimension's offerings: AI-powered chat and voice agents, workflow automation, integrations. 4. If they request a demo, collect their name, email, and preferred time, and confirm the booking. 5. Handle unclear queries with a helpful fallback, asking the user to rephrase. Always end the call with a thank you and a warm closing message. Website URL: https://www.omnidim.io/ ``` ### Web scraping [#web-scraping] Automatically scrape the website and create a knowledge base from the URL provided in the prompt. * The bot will automatically extract information from the website * Create a structured knowledge base for the agent * Attach the knowledge base to the bot for real-time information retrieval Web scraping data ### Cal.com integration [#calcom-integration] Integrate Cal.com for sales or demo call booking. * Navigate to the Integrations tab * Click **Connect** next to Cal.com * Complete the integration steps * Once connected, the calendar will be automatically attached to the agent Cal.com Integration See the [Cal.com integration](/docs/integrations/cal-com) guide for the full walkthrough. ### Slack integration for post-call [#slack-integration-for-post-call] Integrate Slack for post-call delivery. * Navigate to the Integrations tab * Click **Connect** next to Slack * Complete the integration steps * Navigate to the **Post-Call** tab * Choose **Slack** as the delivery method * Choose the channel where you want to send the message * Configure the details to send (full conversation, summary, extracted variables, etc.) Slack post-call delivery See [Slack integration](/docs/integrations/slack) and [post-call actions](/docs/dashboard-guides/post-call) for full details. ### Add a Make.com webhook [#add-a-makecom-webhook] Add a Make.com webhook link and configure the scenario to update an Airtable database. * Create a webhook in Make.com * Set the webhook URL in your post-call agent configuration * Test the webhook by sending a sample payload Add Make.com webhook URL to post-call Extracted variables Make.com webhook See [Make, Zapier, n8n, GHL](/docs/integrations/zapier-make-n8n) and [post-call actions](/docs/dashboard-guides/post-call) for the full configuration reference. ### Final testing and monitoring [#final-testing-and-monitoring] * Re-test the agent via chat and web call to confirm all functionality * Validate the booking flow, fallback responses, and knowledge base retrieval * Navigate to the Call Logs section to monitor call interactions with evaluation details # Real estate agent (API) (/docs/examples/real-estate) > Build a voice agent that handles property inquiries, schedules viewings, and manages client interactions via the API. Real estate agent overview Build an inbound real estate voice agent end to end with the OmniDimension Python SDK. The agent presents property listings, matches client requirements, schedules viewings, and tracks call analytics. ### Get your API key [#get-your-api-key] Get an API key from the OmniDimension dashboard, in the API section. API key ### Create the real estate assistant [#create-the-real-estate-assistant] Start by creating your voice agent with real estate-specific configuration. ```python from omnidimension import Client client = Client("your_api_key_here") real_estate_agent = client.agent.create( name="Sarah - Premier Properties Assistant", welcome_message="Hello! This is Sarah from Premier Properties. I'm here to help you find your dream home or assist with any property inquiries. How can I help you today?", context_breakdown=[ { "title": "Available Properties", "body": """RESIDENTIAL PROPERTIES: - 3BR Luxury Condo: $450,000 | 2,200 sq ft | Downtown - 4BR Family Home: $650,000 | 3,500 sq ft | Suburbs - 2BR Starter Home: $325,000 | 1,800 sq ft | City Center RENTAL PROPERTIES: - 2BR Apartment: $2,200/month | 1,200 sq ft | Downtown - 3BR Townhouse: $3,500/month | 2,000 sq ft | Suburbs - 1BR Studio: $1,500/month | 800 sq ft | City Center COMMERCIAL PROPERTIES: - Office Space: $25/sq ft | 5,000 sq ft | Business District - Retail Space: $30/sq ft | 3,000 sq ft | Shopping Center - Warehouse: $15/sq ft | 10,000 sq ft | Industrial Zone""" }, { "title": "Inquiry Process", "body": "1. Greet client professionally 2. Understand property requirements 3. Match with available listings 4. Schedule viewings if interested 5. Collect contact information 6. Provide market information" }, { "title": "Service Areas", "body": "We serve the entire metropolitan area including Downtown, Suburbs, and surrounding neighborhoods. Our agents specialize in residential, commercial, and rental properties." } ], call_type="Incoming", voice={ "provider": "eleven_labs", "voice_id": "EXAVITQu4vr4xnSDxMaL" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.6 } ) agent_id = real_estate_agent["json"]["id"] print(f"Real estate agent created with ID: {agent_id}") ``` **Key components:** * `welcome_message`: professional greeting establishing expertise * Property listings: comprehensive inventory with prices and details * Inquiry process: step-by-step workflow for handling inquiries * Service areas: geographic coverage information * Voice configuration: professional, trustworthy voice ### Upload property documentation [#upload-property-documentation] Enhance your agent's knowledge by uploading property brochures. ```python import base64 def upload_property_documentation(agent_id, brochure_path): """Upload property brochure to knowledge base and attach to agent""" # Read and encode the PDF file with open(brochure_path, "rb") as file: file_data = base64.b64encode(file.read()).decode('utf-8') # Upload to knowledge base kb_response = client.knowledge_base.create(file_data, "Property_Brochure.pdf") file_id = kb_response["json"]["file"]["id"] # Attach to agent attach_response = client.knowledge_base.attach([file_id], agent_id) print(f"Property documentation uploaded successfully. File ID: {file_id}") return file_id # Upload your property brochure brochure_file_id = upload_property_documentation(agent_id, "property_brochure.pdf") ``` ### Handle incoming client calls [#handle-incoming-client-calls] Your agent is now ready to handle incoming calls. When clients call your office number, the agent will: * Greet clients with the welcome message * Understand requirements (property type, budget, location) * Present matching properties based on client preferences * Schedule viewings for interested properties * Collect contact information for follow-up * Provide market information and next steps ### Monitor performance and analytics [#monitor-performance-and-analytics] Track your real estate agent's performance. ```python def get_real_estate_analytics(agent_id, days=7): """Get comprehensive analytics for real estate agent""" # Get recent call logs call_logs = client.call.get_call_logs(agent_id=agent_id, page_size=100) calls_data = call_logs.get('data', []) # Calculate metrics total_calls = len(calls_data) successful_calls = sum(1 for call in calls_data if call.get('status') == 'completed') average_duration = sum(call.get('duration', 0) for call in calls_data) / total_calls if total_calls > 0 else 0 # Generate report analytics_report = { "period_days": days, "total_calls": total_calls, "successful_calls": successful_calls, "completion_rate": f"{(successful_calls/total_calls*100):.1f}%" if total_calls > 0 else "0%", "average_call_duration": f"{average_duration:.1f} seconds", "recent_calls": calls_data[:5] # Last 5 calls } return analytics_report # Get weekly performance report weekly_report = get_real_estate_analytics(agent_id, days=7) print("Real Estate Agent Performance Report") print(f"Total Calls: {weekly_report['total_calls']}") print(f"Successful Calls: {weekly_report['successful_calls']}") print(f"Completion Rate: {weekly_report['completion_rate']}") print(f"Average Call Duration: {weekly_report['average_call_duration']}") ``` ### Update agent configuration [#update-agent-configuration] Modify your agent settings as needed. ```python def update_agent_details(agent_id, **kwargs): """Update agent configuration""" response = client.agent.update(agent_id, **kwargs) print(f"Agent updated successfully") return response # Update welcome message for weekend hours update_agent_details( agent_id, welcome_message="Good morning! Welcome to Premier Properties. Our weekend open houses are ready! How can I help you find your dream home?" ) # Switch back to incoming calls update_agent_details( agent_id, call_type="Incoming" ) ``` ### Complete setup [#complete-setup] Here's the complete setup for a production-ready real estate agent. ```python from omnidimension import Client import base64 class RealEstateVoiceAgent: def __init__(self, api_key): self.client = Client(api_key) self.agent_id = None def setup_complete_real_estate_agent(self): """Set up a complete real estate voice agent with all features""" # Create the main agent agent = self.client.agent.create( name="Premier Properties - Voice Assistant", welcome_message="Hello! This is Sarah from Premier Properties. I'm here to help you find your dream home or assist with any property inquiries. How can I help you today?", context_breakdown=[ { "title": "Complete Property Listings", "body": """ RESIDENTIAL PROPERTIES: - 3BR Luxury Condo: $450,000 | 2,200 sq ft | Downtown | Amenities: Pool, Gym, Doorman - 4BR Family Home: $650,000 | 3,500 sq ft | Suburbs | Features: Large Yard, 2-Car Garage - 2BR Starter Home: $325,000 | 1,800 sq ft | City Center | Features: Renovated Kitchen, Hardwood Floors - 5BR Executive Home: $850,000 | 4,200 sq ft | Gated Community | Features: Home Office, Pool RENTAL PROPERTIES: - 2BR Apartment: $2,200/month | 1,200 sq ft | Downtown | Utilities Included - 3BR Townhouse: $3,500/month | 2,000 sq ft | Suburbs | Pet-Friendly - 1BR Studio: $1,500/month | 800 sq ft | City Center | Furnished Option Available - 4BR House: $4,000/month | 2,800 sq ft | Suburbs | 12-month Lease COMMERCIAL PROPERTIES: - Office Space: $25/sq ft | 5,000 sq ft | Business District | Class A Building - Retail Space: $30/sq ft | 3,000 sq ft | Shopping Center | High Foot Traffic - Warehouse: $15/sq ft | 10,000 sq ft | Industrial Zone | Loading Docks - Restaurant Space: $35/sq ft | 2,500 sq ft | Downtown | Former Restaurant """ }, { "title": "Client Service Guidelines", "body": "Always be professional and helpful. Listen carefully to client requirements. Suggest properties that match their needs. Offer virtual tours when possible. Follow up within 24 hours of initial inquiry." }, { "title": "Operational Details", "body": "Office hours: 9 AM - 7 PM weekdays, 10 AM - 5 PM weekends. Service areas: Downtown, Suburbs, and surrounding neighborhoods. Specialties: Residential, Commercial, and Rental properties. Viewing appointments: 1-hour slots." } ], call_type="Incoming", voice={ "provider": "eleven_labs", "voice_id": "EXAVITQu4vr4xnSDxMaL" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.6 } ) self.agent_id = agent["json"]["id"] print(f"Real estate agent created: {self.agent_id}") return self.agent_id def get_performance_summary(self): """Get a quick performance summary""" if not self.agent_id: return "No agent configured" calls = self.client.call.get_call_logs(agent_id=self.agent_id, page_size=50) total_calls = len(calls.get('data', [])) return { "agent_id": self.agent_id, "total_calls_handled": total_calls, "status": "Active and ready for inquiries!" } def upload_property_brochure(self, brochure_path): """Upload property brochure to knowledge base""" if not self.agent_id: print("Please create agent first") return None with open(brochure_path, "rb") as file: file_data = base64.b64encode(file.read()).decode('utf-8') kb_response = self.client.knowledge_base.create(file_data, "Property_Brochure.pdf") file_id = kb_response["json"]["file"]["id"] self.client.knowledge_base.attach([file_id], self.agent_id) print(f"Property brochure uploaded: {file_id}") return file_id # Initialize and deploy real estate agent real_estate = RealEstateVoiceAgent("your_api_key_here") agent_id = real_estate.setup_complete_real_estate_agent() # Upload brochure if you have a PDF # real_estate.upload_property_brochure("property_brochure.pdf") print("\nYour real estate voice agent is now live!") print(f"Agent ID: {agent_id}") ``` ## Key features [#key-features] ### Property management [#property-management] * Complete property listings with prices and details * Property matching through natural conversation * Viewing scheduling and coordination * Property information and market updates ### Call handling [#call-handling] * Incoming client calls for property inquiries * Natural conversation flow * Professional real estate service experience * Efficient lead qualification ### Analytics and monitoring [#analytics-and-monitoring] * Call volume tracking * Call completion rates * Average call duration * Detailed call logs and history ### Knowledge base [#knowledge-base] * Upload property brochures for enhanced knowledge * Attach multiple documents to your agent * Comprehensive property and market information * Easy knowledge management ### Agent management [#agent-management] * Update agent configuration anytime * Modify welcome messages and context * Real-time agent performance monitoring * Easy property listing updates # Recruitment screening bot (/docs/examples/recruit-bot) > Build an outbound voice agent for initial job applicant screening calls, with bulk call campaigns. Recruitment screening bot overview Walk through building an outbound recruitment bot. The agent calls candidates, runs through screening questions, captures structured data, and pushes results to your webhook. Ships with a bulk-call workflow for recruitment drives. ### Write the prompt [#write-the-prompt] Start by writing a clear initial prompt that defines the purpose of your recruitment bot. Click the **Enhance Prompt** button to refine your prompt. Writing the initial prompt ### Answer clarifying questions [#answer-clarifying-questions] The platform will prompt you with clarifying questions about your recruitment process. Answer these precisely to help tailor the voice agent for your specific recruitment workflow. You can select from provided options or write your own custom answers if the options don't match your needs. Answering clarifying questions ### Automatic agent creation [#automatic-agent-creation] Based on your prompt and answers, the system generates an initial version of RecruitBot. This includes greeting and availability confirmation, screening questions, candidate data extraction, and call summary generation. Automatic agent creation ### Review and customize agent details [#review-and-customize-agent-details] * Navigate to the **Details** section of the platform * Edit the welcome message to align with your company's tone and branding * Review and refine each step of the screening process (greeting, availability check, screening questions) * Add additional prompts or sections to handle edge cases or specific recruitment scenarios Customizing agent details ### Test the agent [#test-the-agent] Use the platform's testing tools in the top-right corner. * **Test With Chat** to validate text-based conversation flows * **Test With Web Call** to simulate real voice calls * RecruitBot is an outgoing agent that will call candidates directly * During testing, you'll need to provide candidate details as call context Outgoing agent Test with web call interface, with context setup ### Configure model, STT, voice, and personality [#configure-model-stt-voice-and-personality] Adjust your agent settings for optimal performance. * **Model**: choose GPT-4.0 for sophisticated conversation handling * **Speech-to-Text**: select Deepgram with the Nova 3 model and enable advanced features such as number conversion, punctuation, smart formatting * **Text-to-Speech**: choose Eleven Labs voice (Jessica S, Husky American Female) for a professional tone * **Personality**: set to **Professional** to maintain appropriate tone for recruitment calls Configuring the language model Setting up voice Setting up personality ### Knowledge base [#knowledge-base] Equip your bot with job or company-specific knowledge. * Upload relevant documents (job descriptions, company profiles, benefits information) * These documents help the bot answer candidate queries accurately * Set rules for when the knowledge base should be utilized during calls Knowledge base setup ### Post-call actions [#post-call-actions] Configure what happens after each screening call. * Go to the **Post Call** section * Select **Webhook Delivery** as a post-call action * Add your webhook URL to receive call data * Choose what to include: call summary, full transcript, and extracted candidate information (job status, location preference, notice period, salary expectations, experience) * After the call ends, the entire payload will be sent to your webhook URL. From there you can use your own logic to process the data and take appropriate actions. Post-call settings ### Final testing and monitoring [#final-testing-and-monitoring] * Perform a complete test run to validate all screening questions * Test fallback responses and human escalation flow * Verify TTS clarity and pacing for a natural conversation experience * Use **Call Logs** to track conversations, review AI evaluations, and refine the bot based on real interactions ### Deployment [#deployment] Deploy your RecruitBot to start screening candidates. * Click the **Deploy** button in the top-right corner * Purchase or select an existing phone number * Attach the number to your RecruitBot by clicking **Attach Assistant** Click on Purchase Number Attach your number to the bot ### Set up a bulk call campaign [#set-up-a-bulk-call-campaign] After deploying your RecruitBot, you can set up bulk call campaigns to screen multiple candidates efficiently. See [bulk calls](/docs/bulk-calls) for the full reference. * Navigate to the **Bulk Call** section in the dashboard * Create a new campaign with a descriptive name for your recruitment drive * Select your RecruitBot's phone number from the dropdown menu * Prepare a CSV file with candidate information including: * Required: `phone_number` column with country code (for example, 1 for US, 91 for India) * Optional but recommended: name, position\_applied, resume\_link, and other relevant context fields * Upload your CSV file (max 10MB) and review the preview to ensure data is correctly formatted * Click **Create Campaign** to schedule all calls and track results in the campaign dashboard Bulk call campaign setup # Restaurant agent (API) (/docs/examples/restaurant) > Build a voice agent that handles restaurant orders, reservations, and outbound marketing campaigns. Restaurant agent overview Build a restaurant voice agent end to end with the OmniDimension Python SDK. The agent takes inbound orders from a menu, confirms delivery details, and dispatches outbound promotional calls. ### Get your API key [#get-your-api-key] Get an API key from the OmniDimension dashboard, in the API section. API key ### Create the restaurant assistant [#create-the-restaurant-assistant] Start by creating your voice agent with restaurant-specific configuration. ```python from omnidimension import Client client = Client("your_api_key_here") restaurant_agent = client.agent.create( name="Arnav - Restaurant Assistant", welcome_message="Namaste! This is Arnav from Spice Garden Restaurant. I'd be happy to take your order for authentic North Indian cuisine. How may I help you today?", context_breakdown=[ { "title": "Restaurant Menu", "body": """APPETIZERS: Vegetable Samosas ₹120 | Paneer Tikka ₹220 | Chicken Tikka ₹250 | Aloo Chaat ₹150 MAIN COURSES: Butter Chicken ₹320 | Palak Paneer ₹280 | Dal Makhani ₹240 | Chicken Curry ₹300 | Lamb Biryani ₹380 BREADS & RICE: Plain Naan ₹50 | Garlic Naan ₹70 | Butter Naan ₹60 | Basmati Rice ₹80 | Vegetable Biryani ₹220 BEVERAGES: Lassi ₹80 | Masala Chai ₹40 | Fresh Lime Soda ₹60""" }, { "title": "Order Process", "body": "1. Greet customer warmly 2. Take complete order with quantities 3. Confirm items and total 4. Collect delivery address 5. Ask for payment preference 6. Provide order confirmation and estimated delivery time" }, { "title": "Restaurant Policies", "body": "Delivery available within 5km radius. Minimum order ₹200. Free delivery above ₹500. Payment methods: Cash on delivery, UPI, Card. Standard delivery time: 30-45 minutes." } ], call_type="Incoming", voice={ "provider": "eleven_labs", "voice_id": "JBFqnCBsd6RMkjVDRZzb" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.7 } ) agent_id = restaurant_agent["json"]["id"] print(f"Restaurant agent created with ID: {agent_id}") ``` **Key components:** * `welcome_message`: first impression when customers call * Menu context: complete menu with prices in Indian Rupees * Order process: step-by-step workflow for handling orders * Restaurant policies: delivery rules and payment options * Voice configuration: natural-sounding voice ### Upload menu knowledge base [#upload-menu-knowledge-base] Enhance your agent's knowledge by uploading your restaurant's menu PDF. ```python import base64 def upload_restaurant_menu(agent_id, menu_pdf_path): """Upload menu PDF to knowledge base and attach to agent""" # Read and encode the PDF file with open(menu_pdf_path, "rb") as file: file_data = base64.b64encode(file.read()).decode('utf-8') # Upload to knowledge base kb_response = client.knowledge_base.create(file_data, "Restaurant_Menu.pdf") file_id = kb_response["json"]["file"]["id"] # Attach to agent attach_response = client.knowledge_base.attach([file_id], agent_id) print(f"Menu uploaded successfully. File ID: {file_id}") return file_id # Upload your restaurant menu menu_file_id = upload_restaurant_menu(agent_id, "restaurant_menu.pdf") ``` ### Handle incoming customer calls [#handle-incoming-customer-calls] Your agent is now ready to handle incoming calls. When customers call your restaurant number, the agent will: * Greet customers with the welcome message * Present menu options based on customer preferences * Take detailed orders with quantities and special requests * Calculate totals including taxes and delivery charges * Collect delivery information and confirm address * Ask for payment preference (cash, UPI, card) * Provide confirmation with estimated delivery time ### Dispatch outbound marketing calls [#dispatch-outbound-marketing-calls] Reach out to customers with promotional offers. ```python def launch_marketing_campaign(agent_id, customer_list, offer_message): """Launch outbound marketing calls to customer list""" # Update agent for marketing calls marketing_message = f"Hello! This is Arnav from Spice Garden Restaurant. {offer_message} Would you like to place an order today?" client.agent.update( agent_id, welcome_message=marketing_message, call_type="Outgoing" ) # Dispatch calls to customers call_results = [] for customer_phone in customer_list: try: call_response = client.call.dispatch_call( agent_id=agent_id, to_number=customer_phone, call_context="Marketing campaign - Special dinner offer" ) call_results.append({ "phone": customer_phone, "status": "dispatched", "call_id": call_response.get("call_id") }) except Exception as e: call_results.append({ "phone": customer_phone, "status": "failed", "error": str(e) }) return call_results # Launch dinner special campaign dinner_customers = [ "+919876543210", "+919876543211", "+919876543212" ] offer_text = "We have a special 20% discount on our dinner combo meals today!" campaign_results = launch_marketing_campaign(agent_id, dinner_customers, offer_text) print("Marketing campaign launched:") for result in campaign_results: print(f"{result['phone']}: {result['status']}") ``` ### Monitor performance and analytics [#monitor-performance-and-analytics] Track your restaurant agent's performance. ```python def get_restaurant_analytics(agent_id, days=7): """Get comprehensive analytics for restaurant agent""" # Get recent call logs call_logs = client.call.get_call_logs(agent_id=agent_id, page_size=100) calls_data = call_logs.get('data', []) # Calculate metrics total_calls = len(calls_data) successful_calls = sum(1 for call in calls_data if call.get('status') == 'completed') average_duration = sum(call.get('duration', 0) for call in calls_data) / total_calls if total_calls > 0 else 0 # Generate report analytics_report = { "period_days": days, "total_calls": total_calls, "successful_calls": successful_calls, "completion_rate": f"{(successful_calls/total_calls*100):.1f}%" if total_calls > 0 else "0%", "average_call_duration": f"{average_duration:.1f} seconds", "recent_calls": calls_data[:5] # Last 5 calls } return analytics_report # Get weekly performance report weekly_report = get_restaurant_analytics(agent_id, days=7) print("Restaurant Agent Performance Report") print(f"Total Calls: {weekly_report['total_calls']}") print(f"Successful Calls: {weekly_report['successful_calls']}") print(f"Completion Rate: {weekly_report['completion_rate']}") print(f"Average Call Duration: {weekly_report['average_call_duration']}") ``` ### Update agent configuration [#update-agent-configuration] Modify your agent settings as needed. ```python def update_agent_details(agent_id, **kwargs): """Update agent configuration""" response = client.agent.update(agent_id, **kwargs) print(f"Agent updated successfully") return response # Update welcome message for dinner hours update_agent_details( agent_id, welcome_message="Good evening! Welcome to Spice Garden Restaurant. Our dinner specials are ready! How can I help you?" ) # Switch back to incoming calls after marketing campaign update_agent_details( agent_id, call_type="Incoming" ) ``` ### Complete setup [#complete-setup] Here's the complete setup for a production-ready restaurant agent. ```python from omnidimension import Client import base64 class RestaurantVoiceAgent: def __init__(self, api_key): self.client = Client(api_key) self.agent_id = None def setup_complete_restaurant_agent(self): """Set up a complete restaurant voice agent with all features""" # Create the main agent agent = self.client.agent.create( name="Spice Garden - Voice Assistant", welcome_message="Namaste! Welcome to Spice Garden Restaurant. I'm here to help you with orders, reservations, and any questions. How can I assist you today?", context_breakdown=[ { "title": "Full Menu & Pricing", "body": """ APPETIZERS: Veg Samosas ₹120 | Paneer Tikka ₹220 | Chicken Tikka ₹250 | Fish Tikka ₹280 | Aloo Chaat ₹150 MAIN COURSES: Butter Chicken ₹320 | Palak Paneer ₹280 | Dal Makhani ₹240 | Chicken Curry ₹300 | Lamb Biryani ₹380 | Fish Curry ₹300 BREADS: Plain Naan ₹50 | Garlic Naan ₹70 | Butter Naan ₹60 | Roti ₹30 | Kulcha ₹60 RICE: Basmati Rice ₹80 | Veg Biryani ₹220 | Jeera Rice ₹100 BEVERAGES: Sweet Lassi ₹80 | Salted Lassi ₹80 | Masala Chai ₹40 | Fresh Lime ₹60 | Cold Coffee ₹90 DESSERTS: Gulab Jamun ₹100 | Rasmalai ₹120 | Kulfi ₹80 """ }, { "title": "Service Guidelines", "body": "Always be warm and helpful. Suggest popular combinations. Inform about cooking time for special requests. Confirm orders clearly before processing. Handle complaints with empathy." }, { "title": "Operational Details", "body": "Operating hours: 11 AM - 11 PM daily. Delivery radius: 5km. Minimum order: ₹200. Free delivery above ₹500. Estimated delivery: 30-45 minutes. Payment: Cash, UPI, Cards accepted." } ], call_type="Incoming", voice={ "provider": "eleven_labs", "voice_id": "JBFqnCBsd6RMkjVDRZzb" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.7 } ) self.agent_id = agent["json"]["id"] print(f"Restaurant agent created: {self.agent_id}") return self.agent_id def get_performance_summary(self): """Get a quick performance summary""" if not self.agent_id: return "No agent configured" calls = self.client.call.get_call_logs(agent_id=self.agent_id, page_size=50) total_calls = len(calls.get('data', [])) return { "agent_id": self.agent_id, "total_calls_handled": total_calls, "status": "Active and ready for orders!" } def upload_menu_pdf(self, menu_pdf_path): """Upload menu PDF to knowledge base""" if not self.agent_id: print("Please create agent first") return None with open(menu_pdf_path, "rb") as file: file_data = base64.b64encode(file.read()).decode('utf-8') kb_response = self.client.knowledge_base.create(file_data, "Restaurant_Menu.pdf") file_id = kb_response["json"]["file"]["id"] self.client.knowledge_base.attach([file_id], self.agent_id) print(f"Menu PDF uploaded: {file_id}") return file_id # Initialize and deploy restaurant agent restaurant = RestaurantVoiceAgent("your_api_key_here") agent_id = restaurant.setup_complete_restaurant_agent() print("\nYour restaurant voice agent is now live!") print(f"Agent ID: {agent_id}") ``` ## Key features [#key-features] ### Order management [#order-management] * Complete menu with prices and descriptions * Order taking through natural conversation * Menu recommendations and suggestions * Order confirmation with customer details ### Call handling [#call-handling] * Incoming customer calls for orders * Outbound marketing and promotional calls * Natural Hindi/English conversation flow * Professional restaurant service experience ### Analytics and monitoring [#analytics-and-monitoring] * Call volume tracking * Call completion rates * Average call duration * Detailed call logs and history ### Knowledge base [#knowledge-base] * Upload menu PDFs for enhanced knowledge * Attach multiple documents to your agent * Comprehensive menu and policy information * Easy knowledge management ### Agent management [#agent-management] * Update agent configuration anytime * Switch between incoming and outgoing modes * Modify welcome messages and context * Real-time agent performance monitoring # Travel planning and lead generation (/docs/examples/travel-planning-agent) > Build an engaging voice agent that helps users plan trips while collecting lead information for your travel business. Travel planning agent overview Create a complete travel planning voice agent on OmniDimension. The agent suggests destinations, answers questions from a knowledge base, pulls real-time info via web search, and pushes leads to HubSpot via n8n. ### Configure your agent prompt [#configure-your-agent-prompt] Start by creating your voice agent with a travel-specific prompt that establishes the agent's role and capabilities. Agent prompt configuration ### Answer clarifying questions [#answer-clarifying-questions] Complete the agent setup by answering important questions about your travel agency, destination specialties, and lead collection procedures. Configuration questions ### Enhance with knowledge base PDFs [#enhance-with-knowledge-base-pdfs] Upload relevant travel documents to improve your agent's knowledge about destinations, packages, and travel policies. * Go to the **Knowledge Base** tab and click **Upload PDFs** * Upload brochures, guides, package details, and travel requirement documents * Include files covering destinations, pricing, FAQs, and seasonal tips * Ensure each PDF is under 10MB in size Knowledge base configuration ### Enable web search [#enable-web-search] Activate web search to allow your agent to provide real-time information about destinations, weather, attractions, and travel advisories. * Navigate to the **Integrations** tab * Toggle **Web Search** to ON * Configure the search engine as OpenAI or DuckDuckGo * Now your bot can provide real-time information about destinations, weather, and travel advisories Web search configuration ### Test your agent via web call [#test-your-agent-via-web-call] Once your agent is configured, test it through a web call to verify its functionality. * Open the testing interface and click **Test with Web Call** * Try scenarios like destination suggestions, package inquiries, and travel requirements * Ensure lead info is captured accurately and responses feel natural Web call testing interface ### Configure post-call settings [#configure-post-call-settings] Set up data extraction for valuable lead information from customer calls. * Go to the **Post-Call** tab * Set delivery method to **Webhook** for n8n integration * Select data to include: Summary, Full Conversation, Sentiment, Extracted Info * Add variables to extract from each call under **Extracted Variables** Post-call settings ### Set up an n8n webhook for HubSpot [#set-up-an-n8n-webhook-for-hubspot] Create an automation workflow that sends lead information directly to HubSpot CRM. * Log in to your n8n account and create a new workflow * Add a Webhook node as the trigger and configure it with the **POST** method * Copy the generated webhook URL for the OmniDimension post-call settings * Add a HubSpot node and connect your HubSpot account * Select **Create/Update** operation and choose **Tickets** as the resource * Map incoming webhook data to HubSpot contact and ticket fields * Format dates and fields as needed within the workflow * Test the integration to verify successful data flow n8n webhook setup Webhook configuration in n8n HubSpot node in n8n Add information to HubSpot ticket ### Customize voice and behavior [#customize-voice-and-behavior] Personalize your agent's voice and interaction style to match your travel brand's personality. * Go to the **Voice** section and select a provider (for example, Eleven Labs) * Browse and test voices to find an enthusiastic, friendly tone * Filter voices by language, accent, and other traits * Set **Behavior** to **Enthusiastic and Engaging** for travel planning ### Final testing and deployment [#final-testing-and-deployment] Before going live, conduct thorough testing and prepare for deployment. * Conduct testing with various travel scenarios * Verify integrations: n8n to HubSpot, email delivery, web search * Test lead info extraction accuracy for all the fields # Utility bill payment agent (API) (/docs/examples/utility-bill-payment) > Build an outbound voice agent that handles utility bill payments, sends payment reminders, and manages customer interactions. Utility bill payment agent overview Build an outbound payment-reminder voice agent end to end with the OmniDimension Python SDK. The agent verifies accounts, collects payments, and runs bulk reminder campaigns from a CSV. ### Get your API key [#get-your-api-key] Get an API key from the OmniDimension dashboard, in the API section. API key ### Create the payment assistant [#create-the-payment-assistant] Start by creating your voice agent with payment-specific configuration. ```python from omnidimension import Client client = Client("your_api_key_here") payment_agent = client.agent.create( name="BillBot - Payment Assistant", welcome_message="Hello! This is BillBot calling about your utility bill. How can I help you today?", context_breakdown=[ { "title": "Payment Options", "body": """PAYMENT METHODS: - Online: www.pay.com - Phone: 1-800-PAY-BILL - Auto-pay: Available for all accounts - Card Fee: $2.95 per transaction PAYMENT DEADLINES: - Bills due: 21st of each month - Late fee: $25 after 10 days - Disconnection: 30 days past due""" }, { "title": "Account Information", "body": "1. Verify account number 2. Check current balance 3. Confirm payment method 4. Process payment 5. Send confirmation" }, { "title": "Payment Policies", "body": "We accept all major credit cards, bank transfers, and cash payments. Auto-pay customers receive a 2% discount. Payment plans available for qualifying accounts." } ], call_type="Outgoing", voice={ "provider": "eleven_labs", "voice_id": "EXAVITQu4vr4xnSDxMaL" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.4 } ) agent_id = payment_agent["json"]["id"] print(f"Payment agent created with ID: {agent_id}") ``` **Key components:** * `welcome_message`: professional greeting for payment-related calls * Payment options: comprehensive payment methods and deadlines * Account information: step-by-step workflow for payment processing * Payment policies: clear guidelines for payment options * Voice configuration: professional, trustworthy voice ### Process bulk payment reminders [#process-bulk-payment-reminders] Send payment reminders to multiple customers from a CSV file. ```python import csv import json from datetime import datetime def send_payment_reminders(customer_file): """Send payment reminders to customers from CSV file""" with open(customer_file) as f: reader = csv.DictReader(f) for row in reader: context = json.dumps({ "account": row["account"], "name": row["name"], "balance": row["balance"], "due_date": row["due_date"] }) response = client.call.dispatch_call( agent_id=agent_id, to_number=row["phone_number"], call_context=context ) print(f"Reminder sent to {row['name']}: {response['json']['id']}") # Example customer data customers = [ ["phone_number", "account", "name", "balance", "due_date"], ["+15551234567", "5001234567", "John Smith", "125.50", "2024-03-21"], ["+15551234568", "5001234568", "Mary Jones", "89.75", "2024-03-15"] ] # Save customer data with open("customers.csv", "w") as f: writer = csv.writer(f) writer.writerows(customers) # Send reminders send_payment_reminders("customers.csv") ``` ### Handle payment calls [#handle-payment-calls] Your agent is now ready to handle payment-related calls. When customers receive a call, the agent will: * Greet customers with the welcome message * Verify account information for security * Present payment options based on customer preferences * Process payments through the preferred method * Send confirmation with payment details * Schedule follow-up if needed ### Monitor performance and analytics [#monitor-performance-and-analytics] Track your payment agent's performance. ```python def get_payment_analytics(agent_id, days=7): """Get comprehensive analytics for payment agent""" # Get recent call logs call_logs = client.call.get_call_logs(agent_id=agent_id, page_size=100) calls_data = call_logs.get('data', []) # Calculate metrics total_calls = len(calls_data) successful_calls = sum(1 for call in calls_data if call.get('status') == 'completed') average_duration = sum(call.get('duration', 0) for call in calls_data) / total_calls if total_calls > 0 else 0 # Generate report analytics_report = { "period_days": days, "total_calls": total_calls, "successful_calls": successful_calls, "completion_rate": f"{(successful_calls/total_calls*100):.1f}%" if total_calls > 0 else "0%", "average_call_duration": f"{average_duration:.1f} seconds", "recent_calls": calls_data[:5] # Last 5 calls } return analytics_report # Get weekly performance report weekly_report = get_payment_analytics(agent_id, days=7) print("Payment Agent Performance Report") print(f"Total Calls: {weekly_report['total_calls']}") print(f"Successful Calls: {weekly_report['successful_calls']}") print(f"Completion Rate: {weekly_report['completion_rate']}") print(f"Average Call Duration: {weekly_report['average_call_duration']}") ``` ### Update agent configuration [#update-agent-configuration] Modify your agent settings as needed. ```python def update_agent_details(agent_id, **kwargs): """Update agent configuration""" response = client.agent.update(agent_id, **kwargs) print(f"Agent updated successfully") return response # Update welcome message for payment deadline update_agent_details( agent_id, welcome_message="Hello! This is BillBot. Your utility bill payment is due in 3 days. Would you like to make a payment now?" ) # Switch back to outgoing calls update_agent_details( agent_id, call_type="Outgoing" ) ``` ### Complete setup [#complete-setup] Here's the complete setup for a production-ready payment agent. ```python from omnidimension import Client import base64 import csv import json class UtilityBillPaymentAgent: def __init__(self, api_key): self.client = Client(api_key) self.agent_id = None def setup_complete_payment_agent(self): """Set up a complete utility bill payment voice agent with all features""" # Create the main agent agent = self.client.agent.create( name="BillBot - Utility Payment Assistant", welcome_message="Hello! This is BillBot from City Utilities. I'm calling about your utility bill. How can I assist you today?", context_breakdown=[ { "title": "Complete Payment Options", "body": """ PAYMENT METHODS: - Online: www.cityutilities.com/pay - Phone: 1-800-PAY-BILL (1-800-729-2455) - Auto-pay: Available for all accounts with 2% discount - Mobile App: Available on iOS and Android - Mail: P.O. Box 12345, City, State 67890 - In-person: 123 Main Street, City, State 67890 PAYMENT DEADLINES: - Bills due: 21st of each month - Grace period: 5 days - Late fee: $25 after 10 days - Disconnection warning: 20 days past due - Disconnection: 30 days past due - Reconnection fee: $50 """ }, { "title": "Customer Service Guidelines", "body": "Always be professional and helpful. Listen carefully to customer concerns. Offer payment plans when appropriate. Verify identity before discussing account details. Provide clear payment instructions." }, { "title": "Operational Details", "body": "Call center hours: 8 AM - 8 PM weekdays, 9 AM - 5 PM weekends. Payment processing time: Immediate for online/phone, 1-2 business days for mail. Auto-pay enrollment available during call. Payment plans require supervisor approval for balances over $500." } ], call_type="Outgoing", voice={ "provider": "eleven_labs", "voice_id": "EXAVITQu4vr4xnSDxMaL" }, model={ "provider": "anthropic", "model": "gpt-4o-mini", "temperature": 0.4 } ) self.agent_id = agent["json"]["id"] print(f"Payment agent created: {self.agent_id}") return self.agent_id def get_performance_summary(self): """Get a quick performance summary""" if not self.agent_id: return "No agent configured" calls = self.client.call.get_call_logs(agent_id=self.agent_id, page_size=50) total_calls = len(calls.get('data', [])) return { "agent_id": self.agent_id, "total_calls_handled": total_calls, "status": "Active and ready for payment processing!" } def send_bulk_payment_reminders(self, customer_file): """Send payment reminders to customers from CSV file""" if not self.agent_id: print("Please create agent first") return None results = [] with open(customer_file) as f: reader = csv.DictReader(f) for row in reader: context = json.dumps({ "account": row["account"], "name": row["name"], "balance": row["balance"], "due_date": row["due_date"] }) try: response = self.client.call.dispatch_call( agent_id=self.agent_id, to_number=row["phone_number"], call_context=context ) results.append({ "name": row["name"], "status": "success", "call_id": response["json"]["id"] }) except Exception as e: results.append({ "name": row["name"], "status": "failed", "error": str(e) }) return results # Initialize and deploy payment agent payment_agent = UtilityBillPaymentAgent("your_api_key_here") agent_id = payment_agent.setup_complete_payment_agent() print("\nYour utility bill payment voice agent is now live!") print(f"Agent ID: {agent_id}") ``` ## Key features [#key-features] ### Payment management [#payment-management] * Multiple payment methods and options * Automated payment reminders * Payment processing through conversation * Payment confirmation and receipts ### Call handling [#call-handling] * Outbound payment reminder calls * Natural conversation flow * Professional payment service experience * Secure account verification ### Analytics and monitoring [#analytics-and-monitoring] * Call volume tracking * Call completion rates * Average call duration * Detailed call logs and history ### Agent management [#agent-management] * Update agent configuration anytime * Modify welcome messages and context * Real-time agent performance monitoring * Easy payment option updates # Authentication (/docs/get-started/authentication) > How to obtain an OmniDimension API key and configure it for the SDK. To use the OmniDimension SDK, you need an API key. Generate and manage your keys from the [API management dashboard](https://omnidim.io/api-management). ## Setting up your API key [#setting-up-your-api-key] We recommend storing your API key as an environment variable for security: ```bash export OMNIDIM_API_KEY="your_api_key_here" ``` ```bat set OMNIDIM_API_KEY=your_api_key_here ``` ```powershell $env:OMNIDIM_API_KEY="your_api_key_here" ``` Once the variable is set, pass it to the SDK: ```ts import OmniDimension from '@omnidim-ai/sdk'; const client = new OmniDimension({ apiKey: process.env.OMNIDIM_API_KEY }); ``` ```python import os from omnidimension import Client client = Client(os.environ['OMNIDIM_API_KEY']) ``` # Quickstart (/docs/get-started/quickstart) > Build with OmniDimension from an AI client, your backend, or the dashboard. Pick how you want to build. - [Connect an AI client (MCP)](/docs/mcp) — Use OmniDimension from Claude, Cursor, Windsurf, or any MCP client. No code. - [TypeScript SDK](/docs/sdks/typescript) — Call the API from a Node backend. - [Python SDK](/docs/sdks/python) — Call the API from Python. - [REST API](/docs/api-reference) — Call the HTTP API directly. ## Your first request [#your-first-request] Get an API key from the [API management dashboard](https://omnidim.io/api-management) and set it as `OMNIDIM_API_KEY` (see [Authentication](/docs/get-started/authentication)). Then list the agents on your account: ```bash npm install @omnidim-ai/sdk ``` ```ts import OmniDimension from '@omnidim-ai/sdk'; const client = new OmniDimension({ apiKey: process.env.OMNIDIM_API_KEY }); const agents = await client.agents.list(); console.log(agents); ``` ```bash pip install omnidimension ``` ```python import os from omnidimension import Client client = Client(os.environ['OMNIDIM_API_KEY']) print(client.agent.list()) ``` ```bash curl https://omnidim.io/api/v1/agents \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` A list of agents means your key works. An authentication error means the key is wrong or unset; check [Authentication](/docs/get-started/authentication). ## Next steps [#next-steps] - [Configure your agent](/docs/dashboard-guides/configure-your-agent) — Build conversational flow with sections, instructions, and a welcome message. - [API reference](/docs/api-reference) — Every endpoint for agents, calls, numbers, knowledge base, and more. # Cal.com (/docs/integrations/cal-com) > Integrate Cal.com with your voice AI agent to schedule meetings during a call. Cal.com integration allows your agent to access your calendar and schedule meetings on your behalf during a call, so clients can book a time without leaving the conversation. ## Key benefits [#key-benefits] * Automated meeting scheduling * Calendar availability checking * Streamlined appointment booking process ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. Finding Cal.com in dashboard ### Connect Cal.com [#connect-calcom] Find the Cal.com card and click **Connect**, then configure your integration (see configuration details below). Clicking Connect ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Go to your agent's edit page and select the **Integrations** tab. ### Connect Cal.com [#connect-calcom-1] Under **Connect New Integrations**, find Cal.com and click **Connect**. Configure your integration. The integration will automatically attach to your agent. Connecting from agent edit page ## Setup option 3: from the chat interface [#setup-option-3-from-the-chat-interface] ### Ask in chat [#ask-in-chat] Use natural language like "Connect my Cal.com calendar" or "Set up Cal.com integration". A connection widget will automatically appear. ### Add the integration [#add-the-integration] Click **Add integration** to navigate to the integration tab. Under **Connect New Integrations**, find Cal.com and click **Connect**. Configure your integration. ## Configuration guide [#configuration-guide] Enter the following information in the modal during setup: * **Integration Name**: choose a descriptive name (for example, "My Work Calendar") * **API Key**: your Cal.com API key * **Event Type ID**: the ID of the event type for scheduling * **Timezone**: select your preferred timezone Fill in integration details ## Using with your agent [#using-with-your-agent] * Check your calendar availability * Schedule meetings with clients * Manage appointment bookings ## Finding your Cal.com API key [#finding-your-calcom-api-key] * Log in to your Cal.com account * Navigate to **Settings → Developer → API Keys** * Create a new API key with appropriate permissions * Copy the generated key ## Finding your event type ID [#finding-your-event-type-id] * In Cal.com, go to **Event Types** * Select the event type you want to use * The ID is visible in the URL. For example, `https://app.cal.com/event-types/123456`. The `123456` part is your event type ID. Finding Event Type ID # Calendly (/docs/integrations/calendly) > Connect Calendly so your agent can check availability and book meetings during a call. Calendly integration lets your agent schedule meetings on your Calendly account during a call. Unlike Cal.com, Calendly connects with a one-click sign-in (OAuth), so you do not paste an API key. ## Key benefits [#key-benefits] * Automated meeting scheduling during the call * Books against your real Calendly event types * No API key to copy: you authorize with your Calendly login Booking meetings through Calendly requires a paid Calendly plan (Standard or above). On the free plan the connection works but scheduling is rejected by Calendly. ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the [Integrations page](https://omnidim.io/integration) in your dashboard and select the **All Integrations** tab. ### Connect Calendly [#connect-calendly] Find the Calendly card and click **Connect**. Enter an integration name, then click **Connect with Calendly** and sign in to authorize access. ### Pick your event type and timezone [#pick-your-event-type-and-timezone] After authorizing, choose the Calendly **event type** to book against (the list is pulled from your active event types) and set the **timezone**, then save. ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Go to your agent's edit page and select the **Integrations** tab. ### Connect Calendly [#connect-calendly-1] Under **Connect New Integrations**, find Calendly, click **Connect**, enter a name, authorize with Calendly, then pick your event type and timezone. The integration attaches to your agent automatically. ## Configuration [#configuration] During setup you provide: * **Integration name**: a descriptive label (for example, "Sales calls") * **Calendly authorization**: click **Connect with Calendly** and sign in. Access refreshes automatically, so you do not manage tokens. * **Event type**: chosen from a dropdown of your active Calendly event types * **Timezone**: the timezone used for scheduling ## Using with your agent [#using-with-your-agent] * Check availability on your Calendly event type * Book meetings during the call * Confirm the time back to the caller # Custom API (/docs/integrations/custom-api) > Connect your agent to any external service using custom API integration. Custom API integration connects your agent to external services through REST APIs, allowing it to fetch data or perform actions using third-party services. ## Key benefits [#key-benefits] * Access external data sources in real-time * Connect to your own backend services * Integrate with virtually any REST API endpoint * Extend your agent's capabilities with custom functionality ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. Connecting from dashboard ### Connect a Custom API [#connect-a-custom-api] Find the Custom API card and click **Connect**. Configure your API (see configuration details below) and test it in the **Test** tab. Custom API config in dashboard ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Go to your agent's edit page and select the **Integrations** tab. ### Connect a Custom API [#connect-a-custom-api-1] Under **Connect New Integrations**, find Custom API and click **Connect**. Configure and test your API. The integration will automatically attach to your agent. ## Setup option 3: from the chat interface [#setup-option-3-from-the-chat-interface] ### Ask in chat [#ask-in-chat] Use natural language like "Connect my CRM api" or "Connect my Airtable api". A connection widget will automatically appear. ### Add the integration [#add-the-integration] Click **Add integration** to navigate to the integration tab. Under **Connect New Integrations**, find Custom API Integration and click **Connect**. Configure your integration. ## Configuration guide: basic settings [#configuration-guide-basic-settings] * **Integration Name**: choose a descriptive name (for example, "Weather API") * **Description**: explain the API's purpose and usage. This is important for LLM tool calling. * **URL**: full endpoint URL (for example, `https://api.example.com/data`) * **Method**: select HTTP method (GET, POST, PUT, DELETE) ## Configuration guide: headers [#configuration-guide-headers] * Authorization tokens * `Content-Type` specifications * API keys * Custom headers ## Configuration guide: parameters [#configuration-guide-parameters] Specify both query parameters (GET) and body parameters (POST or PUT). * **Name**: parameter identifier * **Description**: purpose of this parameter * **Required**: toggle between optional and required * **Data Type**: select from dropdown * **AI Generated**: toggle whether the AI should dynamically generate values Adding query or body parameters ## Testing your integration [#testing-your-integration] ### Open the test tab [#open-the-test-tab] Navigate to the **Test** tab. ### Run a test [#run-a-test] Enter test values for all parameters and click **Test API** to verify the connection. Running a test before saving integration ### Save the integration [#save-the-integration] After successful testing, click **Save Integration**. ## Using with your agent [#using-with-your-agent] * Once connected, your agent will automatically use this API when appropriate * Usage is determined based on the API description you provide ## Example: Airtable [#example-airtable] Example of using Airtable with custom API integration. Custom API Airtable integration sample ## Example: Supabase user authentication [#example-supabase-user-authentication] Walk through creating a complete Supabase authentication integration step by step. ### Add integration name and description [#add-integration-name-and-description] Make sure descriptions are clear and concise. The AI uses them to understand the integration and decide when to call it. Supabase custom API integration setup ### Configure URL, method, and parameters [#configure-url-method-and-parameters] Set the API URL and HTTP method, then add the query parameters needed for the request. Enable **AI Generated** on the query parameters so the agent can derive the values from the conversation. ### Test and save [#test-and-save] Click next to move to the test step, run a sample request, and save the integration once the response is correct. Supabase custom API testing ## Other example use cases [#other-example-use-cases] * **CRM integration**: retrieve customer data from Salesforce or HubSpot * **Inventory management**: check product availability in real time * **User verification**: authenticate users against internal databases * **Weather data**: fetch current weather for location-based services * **Booking systems**: check availability and make reservations ## Pro tips [#pro-tips] * Provide detailed descriptions for AI-generated parameters * Use specific examples in your descriptions * Test thoroughly before deploying to production # Google Calendar (/docs/integrations/google-calendar) > Integrate Google Calendar with your agent to schedule and manage meetings automatically. Google Calendar integration allows your agent to check calendar availability and book appointments automatically using OAuth-based secure access. ## Key benefits [#key-benefits] * Automated appointment scheduling * Real-time calendar availability sync * Avoid scheduling conflicts * Customize meeting names, duration, and working hours ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. Finding Google Calendar in dashboard ### Connect Google Calendar [#connect-google-calendar] Find the Google Calendar card and click **Connect**. Authorize access through Google OAuth login. Once authenticated, your Google Calendar will connect automatically. Clicking Connect ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Go to your agent's edit page and select the **Integrations** tab. ### Connect Google Calendar [#connect-google-calendar-1] Find **Google Calendar** under **Connect New Integrations** and click **Connect**. Follow the OAuth login process. Once verified, the integration will auto-attach to your agent. ## Configuration modal [#configuration-modal] After connecting, you can customize your calendar settings via the configuration modal. * **Integration Name**: a label to identify this integration * **Description**: purpose of the integration * **Default Meeting Name**: title used for all scheduled events * **Default Meeting Duration**: choose between 15, 30, 60 minutes, etc. * **Business Hours**: set your availability window (start and end time) Customize meeting defaults after connecting Google Calendar ## Using with your agent [#using-with-your-agent] * The agent will automatically fetch available time slots and suggest appointments * Events will only be scheduled within the defined business hours * Meeting invites will be auto-created in your Google Calendar * The user receives a confirmation and calendar link instantly ## Best practices [#best-practices] * Use descriptive integration names to manage multiple calendars * Update business hours as your availability changes * Test the calendar booking flow using simulated queries * Ensure you have granted correct permissions during OAuth login # Google Sheets (/docs/integrations/google-sheets) > Send one row per call to a Google Sheet automatically after each call. Connect Google Sheets to deliver call data to a spreadsheet, one row per call, with no coding. It is a two-part setup: connect your Google account, then attach the sheet as a post-call delivery destination on your agent. ## Step 1: connect your Google account [#step-1-connect-your-google-account] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page (or your agent's **Integrations** tab) and find the Google Sheets card. ### Connect [#connect] Enter an integration name, click **Connect with Google Sheets**, and authorize access in the Google popup. ## Step 2: deliver calls to a sheet [#step-2-deliver-calls-to-a-sheet] ### Open the agent's Post-Call tab [#open-the-agents-post-call-tab] On your agent's edit page, open the **Post-Call** tab and add a delivery configuration. ### Choose Google Sheets [#choose-google-sheets] Set **Delivery method** to Google Sheets, then select the Google account you connected in step 1. ### Point it at a spreadsheet [#point-it-at-a-spreadsheet] Paste the spreadsheet URL or ID and connect it, then pick the worksheet (defaults to Sheet1). ### Choose what to include [#choose-what-to-include] Optionally include the summary, full conversation, sentiment, and extracted variables. Save the configuration. ## What lands in the sheet [#what-lands-in-the-sheet] Each completed call appends one row. Columns always include the call id and date, phone numbers, agent name, direction, status, duration, whether the call was transferred, and the recording link. If you enabled them, the row also carries the summary, sentiment, extracted variables, and full transcript. For bulk campaigns, campaign name and retry count are added too. The header row is managed for you: it is created from the first call's fields, and new columns are added automatically if later calls include new fields. This page covers post-call delivery. There is also a separate "Google Sheets (During Call)" integration that lets the agent read and write a sheet live while the call is happening. # HubSpot (/docs/integrations/hubspot) > Integrate HubSpot with your voice AI agent to automatically manage contacts, deals, and more. The HubSpot integration enables your voice AI agent to sync post-call data into your CRM. You can automatically create and update contacts, deals, and tickets based on extracted variables from the conversation. ## Key benefits [#key-benefits] * One-click OAuth integration * Automatic syncing of leads and deals post-call * Streamlined contact and CRM record creation ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. HubSpot integration setup from dashboard ### Connect HubSpot [#connect-hubspot] Find the HubSpot card and click **Connect**. Enter your integration name and description, then click **Connect with HubSpot**. HubSpot Connect Card ### Authorize via OAuth [#authorize-via-oauth] An OAuth popup will appear. Log in to your HubSpot account and grant access. HubSpot OAuth authorization flow ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the HubSpot integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Navigate to your agent's edit page and select the **Integrations** tab. ### Connect a new HubSpot integration [#connect-a-new-hubspot-integration] Click **Connect New Integration** and choose **HubSpot**. Enter a meaningful integration name and purpose, then click **Connect with HubSpot** and complete OAuth login. The integration will automatically attach to the agent. ## Using with your agent [#using-with-your-agent] * Your agent will automatically push call summary and extracted data to HubSpot * Based on the call flow, it can create new contacts or update existing ones * You can map variables like name, email, phone, intent, and deal size ## Tips for effective integration [#tips-for-effective-integration] * Use clear and consistent naming for integrations * Ensure your HubSpot user has access to create and update contacts and deals * Map only relevant variables to avoid clutter * Test with a sample call to verify record creation in HubSpot # Integrations (/docs/integrations) > Connect OmniDimension to anything. Custom API turns any REST endpoint into an agent action, plus prebuilt connectors for the platforms you already use. If your tool has an API, your agent can use it. Custom API is the universal escape hatch: any REST endpoint becomes an action your agent can call mid-conversation. The prebuilt connectors below are shortcuts for platforms we see most often, but they're not the limit. 🔌 ## Universal connector [#universal-connector] - [Custom API (any REST endpoint)](/docs/integrations/custom-api) — Wire your agent to anything that speaks HTTP. AI-generated parameters, auth headers, dynamic payloads — the works. ## Automation platforms [#automation-platforms] Already running flows in Zapier, Make, n8n, or GHL? Plug OmniDimension into them with webhooks and REST. - [Make, Zapier, n8n, GHL](/docs/integrations/zapier-make-n8n) — Webhook + REST recipes for the popular automation platforms. ## Prebuilt connectors [#prebuilt-connectors] One-click OAuth and prefilled field mappings — use these when your CRM or scheduling tool is on the list. - [Salesforce](/docs/integrations/salesforce) — Push extracted variables to Leads, Contacts, and Opportunities. - [Cal.com](/docs/integrations/cal-com) — Automated meeting scheduling with calendar availability checks. - [Google Calendar](/docs/integrations/google-calendar) — OAuth scheduling, configurable business hours, and meeting defaults. - [HubSpot](/docs/integrations/hubspot) — One-click OAuth. Sync post-call data into contacts, deals, and tickets. - [Slack](/docs/integrations/slack) — Real-time alerts, call summaries, and event-based notifications. Almost everything modern exposes a REST API. Use [Custom API](/docs/integrations/custom-api) and you're connected — no waiting for us to ship a dedicated connector. # Salesforce (/docs/integrations/salesforce) > Connect your Salesforce CRM with your voice AI agent to manage contacts, leads, and opportunities. The Salesforce integration allows your agent to access and update records in your CRM automatically. After each call, OmniDimension can push post-call data to Salesforce objects like Leads, Contacts, or Opportunities. ## Key benefits [#key-benefits] * Automatically update CRM records after a call * Streamline lead management workflows * Create or update contacts, accounts, and opportunities based on extracted variables ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. Salesforce integration dashboard setup ### Connect Salesforce [#connect-salesforce] Find the Salesforce card and click **Connect**. Enter your Salesforce instance details and credentials, then save the integration and test the connection. Configuration screen for Salesforce integration ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the Salesforce integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Go to your agent's edit page and open the **Integrations** tab. ### Connect Salesforce [#connect-salesforce-1] Choose **Salesforce** from the **Connect New Integration** section and click the connect button. Enter your integration name, description, and instance URL. ### Provide credentials [#provide-credentials] Provide your Salesforce username and password, then enter your client ID and client secret from the connected Salesforce app. ### Select objects and save [#select-objects-and-save] Select the Salesforce objects you want to connect to (for example, Lead, Contact, Opportunity). Click **Save** to finish setup and attach the integration to the agent. ## Configuration fields [#configuration-fields] Fields required for configuring the integration * **Integration Name**: choose a recognizable name * **Description**: purpose or details about this integration * **Instance URL**: your Salesforce domain URL (for example, `https://yourcompany.my.salesforce.com`) * **Username and Password**: your Salesforce login credentials * **Client ID and Client Secret**: from your connected Salesforce app * **Connected Objects**: select the Salesforce objects (Leads, Contacts, Opportunities, etc.) you wish to sync ## Using with your agent [#using-with-your-agent] * Extracted data from user calls will be sent to Salesforce automatically * Mapped variables are updated in the selected objects * Conversation history, intent, and sentiment can also be logged ## Tips for a successful integration [#tips-for-a-successful-integration] * Use a dedicated Salesforce integration user account * Ensure the connected app has API access enabled * Map only the necessary fields to avoid data clutter * Test the connection thoroughly using sample calls # Slack (/docs/integrations/slack) > Send automated messages and notifications from your voice AI agent into your Slack workspace. Slack integration lets your agent send real-time messages and notifications directly to your Slack workspace. Use it to alert your team, log events, or notify users within specific channels. ## Key benefits [#key-benefits] * Send real-time alerts and updates to Slack * Keep your team informed about user activity * Trigger notifications based on agent interactions ## Setup option 1: from the main dashboard [#setup-option-1-from-the-main-dashboard] ### Open the integrations page [#open-the-integrations-page] Go to the Integrations page in your dashboard and select the **All Integrations** tab. Finding Slack integration in dashboard ### Connect Slack [#connect-slack] Find the Slack card and click **Connect**. Configure the integration name and description, then complete the OAuth authorization. Clicking Connect ### Attach to your agent [#attach-to-your-agent] Go to your agent's edit page, open the **Integrations** tab, and attach the integration from the dropdown. ## Setup option 2: from agent configuration [#setup-option-2-from-agent-configuration] ### Open the agent's integrations tab [#open-the-agents-integrations-tab] Navigate to your agent's edit page and find the **Integrations** tab. ### Connect with Slack [#connect-with-slack] Find **Slack** under the **Connect New Integrations** section and click **Connect with Slack**. A modal will appear where you can enter the integration name and description. Click **Connect with Slack** to authorize via OAuth and finalize the integration. Connecting from agent edit page ## Using with your agent [#using-with-your-agent] * Trigger Slack messages when a specific user action occurs * Send call summaries or event-based alerts * Notify team members in specific Slack channels ## Best practices [#best-practices] * Name the integration clearly to identify its purpose * Use dedicated Slack channels for different agents or event types * Keep descriptions concise but meaningful for your team * Test the Slack integration by simulating a user event # Make, Zapier, n8n, GHL (/docs/integrations/zapier-make-n8n) > Connect Make, Zapier, n8n, and GHL to your OmniDimension agent to automate workflows. There are two ways you can integrate OmniDimension with automation platforms like Make, Zapier, n8n, and GHL. * **Webhook method**: send data from OmniDimension to Make, Zapier, n8n, or GHL once the call is completed. * **API method**: trigger OmniDimension actions from Make, Zapier, n8n, or GHL. New to webhooks on these platforms? See their docs: [Make](https://help.make.com/webhooks?utm_source=omnidim\&utm_medium=docs), [Zapier](https://zapier.com/apps/webhook/integrations?utm_source=omnidim\&utm_medium=docs), [n8n](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/?utm_source=omnidim\&utm_medium=docs). ## Webhook method: send post-call data to Make, Zapier, n8n, GHL [#webhook-method-send-post-call-data-to-make-zapier-n8n-ghl] ### Open the agent's post-call tab [#open-the-agents-post-call-tab] Navigate to your agent's page in the OmniDimension dashboard and go to the **Post-Call** tab. ### Set the delivery method to webhook [#set-the-delivery-method-to-webhook] Set the delivery method to **Webhook** and paste the webhook URL from Make (HTTP module), Zapier (Webhook trigger), or n8n (Webhook node). Configuring the post-call webhook ### Test the connection [#test-the-connection] Click **Test Connection** to verify and inspect the payload from your automation platform. Webhook payload setup with call summary, full conversation, sentiment analysis, and extracted variables ### Sample webhook payload [#sample-webhook-payload] ```json { "call_id": 24877, "bot_id": 551, "bot_name": "Your Bot Name", "phone_number": "+1234567890", "call_date": "2025-05-22 05:02:54", "user_email": "user@example.com", "call_report": { "summary": "Brief overview of the conversation...", "sentiment": "Positive", "extracted_variables": { "name": "Ravi", "appointment_type": "Dental Checkup" }, "full_conversation": "Full transcript here...", "interactions": [ { "sequence": 1, "user_query": false, "bot_response": "Hello, how can I help you?", "time": "2025-05-22 05:02:42" }, { "sequence": 2, "user_query": "I want a dental appointment", "bot_response": "Sure, I can help with that.", "time": "2025-05-22 05:02:45" } ] } } ``` ### Example: add call logs to an Airtable database using Make.com [#example-add-call-logs-to-an-airtable-database-using-makecom] * Create a webhook in Make.com * Set the webhook URL in your post-call agent configuration following the guide above * Test the webhook by sending a sample payload * For other platforms: Webhook node for n8n, Webhook trigger for Zapier, Workflow Actions Webhook for GHL Make.com scenario that adds call logs to an Airtable database ## API method: call OmniDimension actions from Make, Zapier, n8n, GHL [#api-method-call-omnidimension-actions-from-make-zapier-n8n-ghl] * Use OmniDimension's REST APIs to trigger actions like creating an agent, dispatching calls, or updating the knowledge base * Use HTTP modules and nodes in your automation tool to make requests to OmniDimension API endpoints * Ensure your API key is set correctly in request headers ### Common OmniDimension API endpoints [#common-omnidimension-api-endpoints] * `POST /api/v1/agents` — create a new agent * `POST /api/v1/calls/dispatch` — trigger an outbound call * `POST /api/v1/knowledge_base/create` — add knowledge entries * `GET /api/v1/calls/logs/:id` — retrieve a conversation log ### Example: call dispatch using Make.com [#example-call-dispatch-using-makecom] ### Add an HTTP module [#add-an-http-module] Create a new HTTP module in Make.com and set the HTTP method to **POST**. ### Configure the request [#configure-the-request] Paste the OmniDimension API endpoint for call dispatch. Add the API key in the `Authorization` header. Set the request body to include `agent_id`, `phone_number`, and `call_context`. API call using HTTP module ### Save and test [#save-and-test] Save the module. For other platforms: HTTP Request node for n8n, Workflow Actions Webhook for GHL, Webhook trigger for Zapier. Setting up body for call dispatch ## Tips and best practices [#tips-and-best-practices] * Always test webhooks with tools like webhook.site or RequestBin * Use conditional logic in workflows based on extracted variables * Handle failures and retries in automation tools for reliability # Overview (/docs/mcp) > Connect Claude, ChatGPT, Cursor, and any MCP-compatible client to your OmniDimension account. Hosted OAuth at mcp.omnidim.io, or local stdio via the npm package. The [Model Context Protocol](https://modelcontextprotocol.io) lets AI clients call tools on your behalf. OmniDimension ships two MCP entry points to the same set of tools (agents, calls, bulk calls, phone numbers, knowledge base, providers): " title="Hosted (cloud, OAuth)" href="/docs/mcp/connect" description="https://mcp.omnidim.io/mcp. Add as a custom MCP server in Claude Desktop, Claude.ai, Cursor, VS Code, Codex. OAuth 2.1 sign-in, no API key handling on your side." /> " title="Local (npm, stdio)" href="/docs/mcp/local-server" description="@omnidim-ai/mcp-server runs as a stdio child process from your editor. Bearer API key in the env block. Use for CI, automation, or stdio-only clients." /> ## Which one should I use? [#which-one-should-i-use] | You're using... | Use this | | ------------------------------------- | ------------------------------------------------------------------- | | Claude Desktop, Claude.ai web | [Hosted](/docs/mcp/connect) | | Claude Code | [Hosted](/docs/mcp/connect/claude-code) (one command) | | Cursor, VS Code, Windsurf | [Hosted](/docs/mcp/connect). These clients speak HTTP MCP natively. | | Codex (CLI, IDE, ChatGPT desktop app) | [Hosted](/docs/mcp/connect/codex) (one command) | | ChatGPT custom connectors | [Hosted](/docs/mcp/connect) | | CI, scripts, headless automation | [Local](/docs/mcp/local-server) | | An MCP client that only speaks stdio | [Local](/docs/mcp/local-server) | If you are not sure, start with the hosted option. It is the path we recommend for everyone shipping interactive AI experiences. ## What you can do over MCP [#what-you-can-do-over-mcp] Every endpoint in the [API reference](/docs/api-reference) is exposed as an MCP tool, generated from the same OpenAPI spec. The main categories: * **Agents.** List, create, update, delete, get. * **Calls.** Dispatch outbound calls, fetch call logs and transcripts. * **Bulk calls.** Create campaigns, monitor live status, cancel. * **Phone numbers.** List, attach to agents, import from Twilio, Exotel, SIP. * **Knowledge base.** Upload files, attach to agents. * **Providers.** List available LLMs, voices, STT, and TTS engines. ## How sign-in works [#how-sign-in-works] The hosted entry point uses a standard browser sign-in. Click connect in your MCP client, approve OmniDimension in the browser, and you're done. Tokens are scoped to your account and can be revoked any time from [omnidim.io/api-management](https://omnidim.io/api-management). The local entry point uses an API key from your account. Drop it in the env block of your client config and the server uses it for every call. See [privacy and telemetry](/docs/mcp/privacy) for what each server sees, what we log, and what we never store. ## Open source [#open-source] Both servers are public on GitHub: * [`omnidim-mcp-cloud`](https://github.com/Omnidim/omnidim-mcp-cloud) (Apache 2.0). Hosted OAuth + HTTP server (FastAPI, Python). * [`omnidim-mcp-server`](https://github.com/Omnidim/omnidim-mcp-server) (MIT). Local stdio server, npm-distributed (TypeScript). Issues and PRs welcome on both. # Local MCP server (/docs/mcp/local-server) > Run the OmniDimension MCP server locally as a stdio child process. Distributed as @omnidim-ai/mcp-server on npm. One command auto-configures every detected MCP client. The local server is the npm-distributed counterpart to the hosted [cloud server](/docs/mcp/connect). It runs on your machine as a stdio child process, reads your OmniDimension API key from an environment variable, and proxies tool calls to the OmniDimension REST API. Use the local server when you want: * An API-key flow instead of OAuth (CI, automation, scripts). * A stdio-only client that cannot speak HTTP MCP. * A one-machine setup with no third-party hop. For everything else, prefer the [hosted cloud server](/docs/mcp/connect). ## Prerequisites [#prerequisites] * Node.js 18 or later. * An OmniDimension API key from [omnidim.io/api-management](https://omnidim.io/api-management). ## One-command setup [#one-command-setup] ```bash npx -y @omnidim-ai/mcp-server setup ``` This detects every MCP client installed on your machine (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code) and writes the right config block into each. It also asks for your API key on first run, validates it, and stores it at `~/.config/omnidim/credentials` for the next run. When `setup` finishes, restart any client that was already open and the OmniDimension tools show up in its tool list. ## Manual install [#manual-install] Skip this section if `setup` worked. Use these snippets only when you want to wire a single client by hand. ### Claude Code [#claude-code] ```bash claude mcp add omnidim-local -e OMNIDIM_API_KEY=sk_xxx -- npx -y @omnidim-ai/mcp-server ``` ### Codex [#codex] Codex keeps its config in `~/.codex/config.toml` (TOML, not JSON), so `setup` does not auto-configure it. Add it with: ```bash codex mcp add omnidim-local --env OMNIDIM_API_KEY=sk_xxx -- npx -y @omnidim-ai/mcp-server ``` ### Claude Desktop, Cursor, Windsurf [#claude-desktop-cursor-windsurf] Add to the client's MCP config file: ```json { "mcpServers": { "omnidim-local": { "command": "npx", "args": ["-y", "@omnidim-ai/mcp-server"], "env": { "OMNIDIM_API_KEY": "sk_xxx" } } } } ``` Config file locations: | Client | Path | | ------------------------ | ----------------------------------------------------------------- | | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` | | Cursor | `~/.cursor/mcp.json` | | Windsurf | `~/.codeium/windsurf/mcp_config.json` | ### VS Code [#vs-code] ```json { "mcp": { "servers": { "omnidim-local": { "type": "stdio", "command": "npx", "args": ["-y", "@omnidim-ai/mcp-server"], "env": { "OMNIDIM_API_KEY": "sk_xxx" } } } } } ``` ## How it works [#how-it-works] On every tool call: 1. The MCP client spawns `npx -y @omnidim-ai/mcp-server` as a child process and pipes JSON-RPC over stdio. 2. The server validates the tool arguments against the OpenAPI-derived schema. 3. The server sends an HTTPS request to the OmniDimension REST API with `Authorization: Bearer ${OMNIDIM_API_KEY}`. 4. The response is shaped into the MCP tool result envelope and returned to the client. The server never writes your key to disk and never sends it to any host other than the OmniDimension API. ## Diagnostics [#diagnostics] The package ships with a `doctor` command that prints a paste-ready report: ```bash npx -y @omnidim-ai/mcp-server doctor ``` It lists detected clients, backend reachability, recent errors, and version info. It never prints your API key. Tool call errors are appended to a local log at `~/.config/omnidim/logs/mcp.log` (redacted, capped at 256 KB, never transmitted). ## Telemetry [#telemetry] Anonymous usage data is sent to help us improve the package: package version, Node version, OS family, install count, and tool call counts by category. No API keys, no tool inputs or outputs, no file paths, no personal info. Disable with: ```bash npx -y @omnidim-ai/mcp-server telemetry disable ``` ## Source [#source] [`@omnidim-ai/mcp-server`](https://www.npmjs.com/package/@omnidim-ai/mcp-server) on npm. Source at [`omnidim-mcp-server`](https://github.com/Omnidim/omnidim-mcp-server). Apache 2.0. # Privacy and telemetry (/docs/mcp/privacy) > What data the MCP servers see, what we log, what we never store. Covers both the hosted cloud server at mcp.omnidim.io and the local npm package. What each MCP server sees on the wire, what it writes to logs, and what it stores at rest. Covers the hosted cloud server and the local npm package. ## Hosted server (`mcp.omnidim.io`) [#hosted-server-mcpomnidimio] **What crosses the wire on a tool call.** Your MCP client sends a JSON-RPC request to `https://mcp.omnidim.io/mcp` with your access token in the `Authorization` header. The cloud validates the token, looks up the upstream credential bound to your account, and forwards the call to the OmniDimension REST API. The response comes back the same way. **What the server sees in memory.** Because the cloud is a proxy, every tool call's arguments, response, and Bearer token pass through process memory while the request is in flight. Standard for any hosted API gateway. Nothing in this section claims otherwise. **What we write to logs.** Per-request structured logs capture: tool name, request ID, your OmniDimension user ID, HTTP method and path, status code, and duration. We use these for debugging and Grafana dashboards. **What we don't write to logs.** A log scrubber runs at the top of the logging pipeline and recursively redacts known-sensitive keys from every event before it's persisted: `api_key`, `access_token`, `refresh_token`, `client_secret`, `code_verifier`, `Authorization`. Tool call request and response bodies also stay out of logs. **What we store at rest, and how.** * Access tokens, refresh tokens, and authorization codes: SHA-256 hashes only. The plaintext only ever exists in the issuing response and the client that holds it. * The upstream OmniDimension API credential that backs your OAuth grant: encrypted with Fernet (AES-128-CBC + HMAC-SHA256), key derived from the service's signing secret. * Tool call inputs and outputs: not persisted at all. **Disconnect.** Revoke at any time from [omnidim.io/api-management](https://omnidim.io/api-management) or from inside your MCP client. The entire grant family is revoked. Subsequent tool calls return `401`. ## Local server (`@omnidim-ai/mcp-server`) [#local-server-omnidim-aimcp-server] **What crosses the wire on a tool call.** Your MCP client spawns the package as a stdio child process and pipes JSON-RPC to its stdin. The server validates arguments, then sends an HTTPS request to the OmniDimension REST API with `Authorization: Bearer ${OMNIDIM_API_KEY}`. No traffic leaves your machine for any other host. **Anonymous telemetry.** The package sends a small set of anonymous events to `https://mcp.omnidim.io/api/telemetry/event`: * Install ID (random UUID, generated locally on first run). * Package version, Node version, OS family, CPU arch. * Setup-step outcomes (succeeded, failed with a category like `config_write_error`). * Per-session tool call counts grouped by tool name and outcome category (e.g. `http_500`, `timeout`). **What we never send.** * Your API key. * Tool call inputs or outputs. * Error messages (only short category codes). * File paths, hostnames, IP addresses, usernames, or anything that could identify you personally. **Local diagnostics log.** When something goes wrong, the real local error message is appended to `~/.config/omnidim/logs/mcp.log` (or platform equivalent). The log is capped at 256 KB, redacts known-sensitive keys, and is never transmitted. The `doctor` command reads it for support reports. **Disable telemetry.** ```bash npx -y @omnidim-ai/mcp-server telemetry disable ``` This writes `~/.config/omnidim/telemetry` with the disable flag. Re-enable with `telemetry enable`. The `DO_NOT_TRACK=1` environment variable is also respected silently. ## Source [#source] Both servers are open source: * [`omnidim-mcp-cloud`](https://github.com/Omnidim/omnidim-mcp-cloud) (hosted, Apache 2.0) * [`omnidim-mcp-server`](https://github.com/Omnidim/omnidim-mcp-server) (local, MIT) Audit the logging and telemetry code directly. The dispatcher is in `app/services/dispatcher.py` on the cloud side and `src/telemetry.ts` on the npm side. # Clients and billing (/docs/omnirelay/clients-and-billing) > Add client workspaces, set your own client pricing, manage your monthly voice minutes, and change plans on OmniRelay. Once your platform is live, you manage everything from your dashboard under **Reseller**. This page covers how clients are added, how you price them, how your minutes work, and how to change plans. ## Adding clients [#adding-clients] Each client gets their own workspace, branded as yours. A client cannot see another client's data, calls, or settings. There are two ways a workspace gets created: 1. **A client signs up on your domain.** When someone signs up on your live domain, a workspace is created for them automatically and they appear in your client list. 2. **You create it yourself.** Under **Reseller → Organizations**, you can create and manage workspaces directly. This is useful when you onboard a client for them rather than asking them to sign up. Your client count starts at zero and goes up as workspaces are created. Each plan includes a set number of client workspaces (Starter, Pro, and Premium differ here). You can see how many you have used on the **Billing** page, under your agency plan. Removing a workspace frees a slot against your plan's client limit. If you are near your limit and want to add a new client, clearing out inactive workspaces is the quickest way to make room without upgrading. ## Setting your pricing [#setting-your-pricing] You decide what to charge your clients. You pay your plan rate to us; what you charge them is entirely yours, and the difference is your margin. You set per-client pricing from your reseller dashboard. Because your cost is the flat plan rate and your revenue is whatever you charge across all your clients, your margin grows as you add clients and as you price closer to the value you deliver rather than to your underlying cost. ## Minutes [#minutes] Each plan includes a monthly grant of voice minutes. You allocate minutes to each client workspace rather than everyone drawing from one shared pool, so you control how much each client gets. You can set a default amount that new client signups start with, and adjust any client's minutes from your reseller dashboard. When a workspace runs out of minutes, its calls pause so you are never billed by surprise. Top up more minutes at any time from the billing page, at your plan's per-minute rate, and calls resume right away. Keep an eye on the usage shown on the **Billing** page so you know when you are approaching your monthly grant, especially around campaigns or busy periods. ## Changing plans [#changing-plans] You can upgrade at any time, and the upgrade takes effect immediately, with the higher client limit and minute grant available right away. You can downgrade once your current client count fits within the lower tier's limit. If you are currently over that limit, reduce your active workspaces first, then the downgrade will apply. This protects your clients from being cut off unexpectedly. ## Related [#related] * [Set up your platform](/docs/omnirelay/set-up-your-platform) * [Connect your domain](/docs/omnirelay/connect-your-domain) * [FAQ](/docs/omnirelay/faq) # Connect your domain (/docs/omnirelay/connect-your-domain) > Point a custom domain at your white-label platform, with per-provider DNS steps and a troubleshooting guide. Your platform runs on your own domain. The setup flow under **Reseller → White Label** shows you the exact DNS record to create (its type, host, and value). This page covers the details: subdomain vs root domain, per-provider steps, and what to do when the check does not pass. The three values the setup shows map directly onto the fields in your provider's DNS form: Anatomy of the DNS record: type, host, and value ## Subdomain or root domain? [#subdomain-or-root-domain] The record type depends on the domain you use. * **Subdomain** (`app.yourbrand.com`): add a **CNAME** pointing to the hostname the setup shows. This is the recommended path, it works on every provider and keeps our origin hidden. * **Root / apex** (`yourbrand.com`): a CNAME is not valid at a root domain, so add an **A record** to the IP the setup shows. If your provider supports **ALIAS / ANAME** (or CNAME flattening), you can point the root at our hostname instead, which keeps the IP hidden. In your provider's **Host** (or **Name**) field, enter just the subdomain label, for example `app` for `app.yourbrand.com`. Some providers want the full hostname (`app.yourbrand.com`) instead. Either works. For a root domain the host is usually `@`. When in doubt, use a subdomain like `app.yourbrand.com`. It is the simplest, most portable option. Whichever you choose, the host should have **only that one record** and **no AAAA (IPv6) record**. We serve over IPv4 only, so a leftover AAAA (some providers add one by default) sends visitors to the wrong place even when your A record is correct. ## Per-provider steps [#per-provider-steps] The setup flow always shows the exact record (type, host, value). Below is where to add it on the most common providers, and which ones offer a root-domain record that keeps our IP hidden. For a subdomain the answer is always a **CNAME** to the setup hostname; the provider differences are really about the **root domain**. ### Cloudflare [#cloudflare] 1. Open **DNS → Records → Add record**. 2. Subdomain: a **CNAME** with the name set to your subdomain and the target set to the setup hostname. Root domain: add a **CNAME at the root** (name `@`), Cloudflare flattens it automatically. 3. **Set the proxy status to "DNS only" (grey cloud), not "Proxied" (orange cloud).** A proxied record stops us from issuing your HTTPS certificate, this is the single most common reason setup fails. Click the cloud icon on the record to toggle it. Cloudflare records: a grey cloud (DNS only) is correct; an orange cloud (proxied) blocks HTTPS ### GoDaddy [#godaddy] 1. **My Products → your domain → DNS → Add**. 2. Subdomain: **CNAME**, Name = your subdomain, Value = the setup hostname. 3. Root domain: GoDaddy has no ALIAS option, so add an **A record** (Name = `@`) to the IP the setup shows. ### Namecheap [#namecheap] 1. **Domain List → Manage → Advanced DNS → Add New Record**. 2. Subdomain: **CNAME Record**, Host = your subdomain, Value = the setup hostname. 3. Root domain: Namecheap supports an **ALIAS Record** (Host = `@`) to the setup hostname, which keeps our IP hidden. If you prefer, an **A Record** to the IP also works. ### AWS Route 53 [#aws-route-53] 1. **Hosted zones → your domain → Create record**. 2. Subdomain: a **CNAME** to the setup hostname. 3. Root domain: an **A record** with **Alias** turned on, routing to the setup hostname. ### Squarespace (and former Google Domains) [#squarespace-and-former-google-domains] 1. **Settings → Domains → your domain → DNS settings**. 2. Subdomain: a **CNAME** to the setup hostname. 3. Root domain: add an **A record** (Host = `@`) to the IP the setup shows. ### Hostinger [#hostinger] 1. **Domains → your domain → DNS / Nameservers → Manage DNS records**. 2. Subdomain: **CNAME**, Name = your subdomain, Target = the setup hostname. 3. Root domain: an **A record** (Name = `@`) to the IP the setup shows. 4. Delete any **AAAA (IPv6) record** on the host. Hostinger often adds one by default, and it will send visitors to the wrong site since we serve over IPv4 only. ### Porkbun [#porkbun] 1. **Domain Management → your domain → DNS Records**. 2. Subdomain: a **CNAME** to the setup hostname. 3. Root domain: Porkbun supports an **ALIAS** record to the setup hostname (keeps the IP hidden), or an **A record** to the IP. ### IONOS [#ionos] 1. **Domains & SSL → your domain → DNS**. 2. Subdomain: a **CNAME** to the setup hostname. 3. Root domain: an **A record** (Host = `@`) to the IP the setup shows. ### Any other provider [#any-other-provider] Use the type the setup shows: a **CNAME** for a subdomain, or for a root domain an **A record** (always works) or an **ALIAS / ANAME** record if your provider offers one. Host is your domain; value is what the setup displays. ## HTTPS is automatic [#https-is-automatic] You do not manage certificates. Once your record is live and pointing at us, we issue and renew the SSL certificate for you, usually within a few minutes, and your platform serves over HTTPS on your domain. On Cloudflare, keep the record **DNS only (grey cloud)**, not proxied. Proxying terminates HTTPS at Cloudflare and blocks our certificate from being issued. This is the most common reason a domain does not go live. ## Troubleshooting [#troubleshooting] Most domains go live on the first try. When one does not, it is almost always one of the causes below. The first two cover the large majority of cases. This is the most common message, and it usually just means the record needs more time. DNS changes take a few minutes (sometimes longer) to spread across the internet. 1. Confirm the record **type, host, and value** match exactly what the setup shows. A trailing space, a wrong host label, or a CNAME where an A record is needed will all cause this. 2. Make sure you saved the record at your provider. Some providers stage changes and require a separate confirm or publish step. 3. Wait a few minutes, then click **Verify and launch** again. Check that the record is **DNS only (grey cloud)**, not **Proxied (orange cloud)**. A proxied record terminates HTTPS at Cloudflare and blocks us from issuing your certificate. This is the single most common reason a Cloudflare domain does not go live. Click the cloud icon on the record to toggle it grey, then verify again. Cloudflare records: a grey cloud (DNS only) is correct; an orange cloud (proxied) blocks HTTPS Check that the record is actually live, independent of our setup. Use a lookup tool such as [dnschecker.org](https://dnschecker.org), or run `dig yourdomain.com` (or `dig app.yourbrand.com` for a subdomain) in a terminal. It should resolve to the value the setup shows. * If the lookup shows **nothing or the old value**, the record has not spread yet, or it was saved incorrectly. Re-check it at your provider and give it more time. * If the lookup shows the **right value** but verification still fails, the record is good. Wait a few minutes for our check to catch up and try again. A root domain (`yourbrand.com`) cannot use a CNAME. Use an **A record** to the IP the setup shows, or an **ALIAS / ANAME** record if your provider supports one. Using a subdomain (`app.yourbrand.com`) avoids this entirely and is the simplest path. The certificate is issued after your domain points at us, so there is a short window right after the record goes live where HTTPS is not ready yet. Give it a few minutes, then re-run **Verify and launch**. If it still looks wrong after that, confirm the record is not proxied (Cloudflare grey cloud) and that only one record exists for that host. This almost always means an **AAAA (IPv6) record** is still pointing somewhere else. A domain can hold both an A record (IPv4) and an AAAA record (IPv6) at once, and most browsers prefer IPv6 when it is available. So even after your A record points at us, a leftover AAAA sends visitors to the old host instead. It is not that the AAAA overrides the A record, the browser simply tries IPv6 first. We serve over IPv4 only, so your domain should have **no AAAA record**. Open your DNS settings, delete any AAAA record for the host, and keep only the A record (or CNAME) the setup shows. A stale AAAA can also stop us from issuing your HTTPS certificate, so removing it fixes both problems at once. If an old A or CNAME record for the same host is still in place, it can conflict with the new one and send traffic to the wrong place. Remove any leftover records for that host so only the one the setup shows remains, then verify again. ## Next [#next] Back in the dashboard, click **Verify and launch** once the record is saved. See [Set up your platform](/docs/omnirelay/set-up-your-platform) for the full setup flow. # FAQ (/docs/omnirelay/faq) > Common questions about running a white-label Voice AI platform with OmniRelay, covering branding, setup, pricing, billing, clients, and going live. The questions below cover what most agencies ask before and after they launch. If something is not answered here, the [Set up your platform](/docs/omnirelay/set-up-your-platform) and [Connect your domain](/docs/omnirelay/connect-your-domain) pages go deeper. ## Branding and white labeling [#branding-and-white-labeling] No. Your platform runs on your own domain, with your logo, your colors, and your email domain. The dashboard your clients sign in to is branded as yours. Your platform name and company name, your logo and favicon, a primary brand color, and your font. You also set the domain your clients use and the email domain your notifications are sent from. A live preview shows each change as you make it. Yes. Branding is not a one-time setup. You can update your logo, colors, name, and font at any time from **Reseller → White Label**, and changes save as you go. Yes. Each client works in their own workspace on your domain. They sign in, manage their own work, and never see another client's data. ## Setup and domains [#setup-and-domains] The branding and domain steps take under ten minutes. After you add the DNS record, it usually spreads across the internet within a few minutes, though it can take up to an hour. Most agencies are live within the hour. No. You add your logo, enter the domain you want to use, create one DNS record at your domain provider, and launch. If you can edit DNS records (or someone on your team can), you have everything you need. Higher tiers include a one-on-one onboarding call if you would like a hand. Either works. A subdomain like `app.yourbrand.com` is the simplest and most portable option, and we recommend it. A root domain (`yourbrand.com`) also works, with a slightly different DNS record. Both are covered in [Connect your domain](/docs/omnirelay/connect-your-domain). No. Once your domain points at us, we issue and renew the HTTPS certificate for you automatically. There is nothing to buy or install. Most of the time the record needs a few more minutes to spread, or one field does not match what the setup shows. The full checklist, including the most common Cloudflare mistake, is in the [troubleshooting section](/docs/omnirelay/connect-your-domain#troubleshooting). ## Pricing and billing [#pricing-and-billing] Yes. You decide what to charge your clients. We charge you your plan rate, and the difference between the two is your margin. You set per-client pricing from your reseller dashboard. You pay one monthly plan rate. Each plan includes a number of client workspaces and a monthly grant of voice minutes. You can see your usage on the **Billing** page under your agency plan. When the balance runs out, calls pause so there are no surprise charges. Top up more minutes anytime from your billing page, at your plan's per-minute rate, and calls resume right away. Yes, at any time. Upgrades take effect immediately. A downgrade applies once your current client count fits within the lower tier's limit. ## Clients [#clients] Clients appear when they sign up on your live domain. You can also create and manage workspaces yourself under **Reseller → Organizations**. Each plan includes a set number of client workspaces, and the tiers differ here. You can see how many you have used on the **Billing** page. If you need more, upgrade to the next tier. Nothing changes for them while they are within your new tier's limit. If you are over the limit, the downgrade applies once your client count fits the lower tier, so no client is cut off unexpectedly. # OmniRelay (/docs/omnirelay) > Sell Voice AI under your own brand with OmniRelay, OmniDimension's platform for agencies: your domain, your branding, your client pricing. OmniRelay lets you sell Voice AI as your own product. Your clients sign in on your domain, see your logo and your emails, and work in a dashboard that looks like yours. It is built for agencies, consultants, and SaaS teams who want to offer Voice AI under their own brand without building it themselves. ## How it works [#how-it-works] 1. **Pick a plan.** Choose the tier with the minutes and client count you need, and pay monthly. 2. **Make it yours.** Add your logo, colors, and email domain, then point your domain at us. 3. **Go live.** Your branded platform runs on your own domain. Add client workspaces and set their minutes. Setup is self-serve and takes about ten minutes. See [Set up your platform](/docs/omnirelay/set-up-your-platform). ## How you earn [#how-you-earn] You pay one plan rate. You decide what to charge your clients. The difference between what you charge and what you pay is your margin. Pricing for each tier is on the [agency pricing page](https://omnidim.io/pricing?tab=agency). ## Next steps [#next-steps] * [Set up your platform](/docs/omnirelay/set-up-your-platform): branding, domain, and going live. * [Connect your domain](/docs/omnirelay/connect-your-domain): DNS steps and troubleshooting. * [Clients and billing](/docs/omnirelay/clients-and-billing): adding clients, pricing, and minutes. * [FAQ](/docs/omnirelay/faq): common questions. # Set up your platform (/docs/omnirelay/set-up-your-platform) > Brand your OmniRelay platform, point your domain at us, and go live for your clients in about ten minutes. Setup runs in your dashboard under **Reseller → White Label**. It is a short guided flow: branding, domain, then launch. Changes save as you go, so you can step away and come back at any time without losing your progress. Before you start, it helps to have your logo file ready and access to your domain provider (where you manage DNS). Everything else you can fill in as you go. ## 1. Pick a plan [#1-pick-a-plan] On the [agency pricing page](https://omnidim.io/pricing?tab=agency), choose the tier with the minutes and number of clients you need, and pay monthly. After payment, click **Set up your platform** to open the setup flow. If you are not sure which tier fits, start with the one that covers your current client count. You can upgrade at any time, and the change takes effect immediately. ## 2. Branding [#2-branding] In the **Branding** step, add the details that make the platform yours: * **Platform name and company name.** What your clients see in the title and footer. * **Logo and favicon.** Your logo appears in the dashboard; the favicon shows in the browser tab. Use a logo with a transparent background so it sits cleanly on both light and dark backgrounds. * **Primary color.** Your brand color, used for buttons and accents. * **Font family.** The typeface used across the dashboard. A live preview shows your changes as you type, so you can see exactly how the platform will look before you go live. You can come back and adjust any of this later. The Branding step: platform name, company name, logo, colors, and font, with a live preview of your dashboard ## 3. Domain [#3-domain] In the **Domain** step, enter the hostname your customers will use. Use a subdomain such as `app.yourbrand.com`. It is the simplest and most portable option, and it works on every domain provider. You can change it later if you need to. A root domain (`yourbrand.com`) also works, with a slightly different DNS record. If you plan to use one, read [Connect your domain](/docs/omnirelay/connect-your-domain) first so you know which record to add. The Domain step: enter the hostname your customers will use ## 4. Point your domain at us [#4-point-your-domain-at-us] In the **DNS and Launch** step, the setup shows you the exact DNS record to create: its type, host, and value. Add that one record at your domain provider, save it, and wait a few minutes for DNS to update. The step shows the exact record, with a copy button on each field: The DNS and Launch step: the Type, Host, and Value to copy into your DNS provider, with Verify and launch Those three values map directly onto the fields in your provider's DNS form: Anatomy of the DNS record: type, host, and value Use a subdomain like `app.yourbrand.com` for the simplest setup. Root domains, Cloudflare, and per-provider steps are covered in [Connect your domain](/docs/omnirelay/connect-your-domain). ## 5. Go live [#5-go-live] Click **Verify and launch**. The moment your domain checks out, your branded platform is live on your own domain over HTTPS, with your clients signing up under your name. There is no extra step and no waiting on us: we issue and renew the SSL certificate for you. If the check does not pass yet, confirm the record matches what the setup shows, give DNS a few more minutes, and try again. The [troubleshooting section](/docs/omnirelay/connect-your-domain#troubleshooting) walks through the common causes. ## 6. Update your branding anytime [#6-update-your-branding-anytime] You do not need everything perfect before you launch, since going live is instant. After launch, the **White Label** page becomes your management view: update your logo, favicon, colors, font, and which default UI to hide from your users whenever you like. Changes apply to your platform on the next refresh, so you can keep refining your brand after you are live. The White Label settings: edit logo, favicon, colors, font, and visibility, with a live preview Here is a launched platform, fully branded for its clients: A launched, fully branded platform on its own domain ## Next [#next] Once you are live, see [Clients and billing](/docs/omnirelay/clients-and-billing) to add client workspaces and set your pricing. # SDKs (/docs/sdks) > Official OmniDimension SDKs for the backend and the browser. OmniDimension offers two kinds of SDK, split by where they run and which key they use. ## Server SDKs [#server-sdks] Run on your backend and call the [REST API](/docs/api-reference) with a secret API key. Use these to build and manage agents, dispatch calls, run campaigns, and handle knowledge bases from your own services. See [Authentication](/docs/get-started/authentication) to get a key. " title="Python" href="/docs/sdks/python" description="The omnidimension package. Install with pip." /> " title="TypeScript" href="/docs/sdks/typescript" description="The @omnidim-ai/sdk package. Typed, server-side, works in JavaScript too." /> ## Client SDKs [#client-sdks] Run in the browser and never see your secret API key. Pick by how much control you want: * **Web widget** (no code): a copy-paste script with a built-in UI, authenticated by a public, agent-scoped widget key. * **Web SDK** (low code): the `@omnidim-ai/client` package. Your server creates a [session](/docs/api-reference/sessions/createSession) with the API key and hands the page a short-lived connection URL; you build your own UI around the `WebSession` class. * **Raw protocol** (full control): speak the [web call protocol](/docs/web-call-protocol) directly from any platform, including iOS, Android, and kiosks. " title="Web widget" href="/docs/sdks/web-widget" description="Embed your agent on any site with a no-code script." /> " title="Web SDK" href="/docs/sdks/web" description="The @omnidim-ai/client package. Voice conversations with your own UI." /> More languages are on the way. If you'd like to see one prioritized, [let us know](https://omnidim.io/contact-us). # Python SDK (/docs/sdks/python) > The omnidimension Python package. Install with pip and start calling the API. The `Client` class is the main entry point for the OmniDimension Python SDK. It handles authentication, request management, and provides access to every API domain. ## Installation [#installation] Install the SDK from PyPI: ```bash pip install omnidimension ``` Package details and version history live at [pypi.org/project/omnidimension](https://pypi.org/project/omnidimension/). ## Initialization [#initialization] Get an API key by following [Authentication](/docs/get-started/authentication), then initialize the Client: ```python import os from omnidimension import Client api_key = os.environ.get('OMNIDIM_API_KEY', 'your_api_key_here') client = Client(api_key) ``` ## Client structure [#client-structure] ```python client.agent # Agent operations client.call # Call operations client.knowledge_base # Knowledge base operations client.phone_number # Phone number operations ``` " title="Agent" href="/docs/api-reference/agents/listAgents" description="Create, retrieve, update, and delete AI voice agents." /> " title="Call" href="/docs/api-reference/calls/dispatchCall" description="Manage call logs and dispatch calls to phone numbers." /> " title="Knowledge base" href="/docs/api-reference/knowledge-base/listKnowledgeBaseFiles" description="Upload, manage, and attach knowledge base files to agents." /> " title="Phone numbers" href="/docs/api-reference/phone-numbers/listPhoneNumbers" description="List, attach, and detach phone numbers from agents." /> ## Error handling [#error-handling] The SDK provides an `APIError` class for API-specific errors: ```python import os from omnidimension import Client, APIError api_key = os.environ.get('OMNIDIM_API_KEY', 'your_api_key_here') client = Client(api_key) try: response = client.agent.list() print(response) except APIError as e: print(f"API Error ({e.status_code}): {e.message}") except Exception as e: print(f"Unexpected error: {str(e)}") ``` ## Client parameters [#client-parameters] # TypeScript SDK (/docs/sdks/typescript) > The @omnidim-ai/sdk package. A typed, server-side SDK for the OmniDimension API. `@omnidim-ai/sdk` is the server-side SDK for the OmniDimension REST API. Fully typed, and works in plain JavaScript too. Run it on your backend with a secret API key. For a browser experience, use the [web widget](/docs/sdks/web-widget). ## Installation [#installation] ```bash npm install @omnidim-ai/sdk ``` Requires Node.js 20 or later. Package details and version history live at [npmjs.com/package/@omnidim-ai/sdk](https://www.npmjs.com/package/@omnidim-ai/sdk). ## Initialization [#initialization] Get an API key by following [Authentication](/docs/get-started/authentication), then create a client: ```ts import OmniDimension from '@omnidim-ai/sdk'; const client = new OmniDimension({ apiKey: process.env.OMNIDIM_API_KEY! }); const agents = await client.agents.list({ pagesize: 10 }); ``` Using plain JavaScript? Drop the `!` and the rest is identical: ```js const client = new OmniDimension({ apiKey: process.env.OMNIDIM_API_KEY }); ``` ## Client structure [#client-structure] ```ts client.agents // Agent operations client.calls // Call dispatch and logs client.bulkCalls // Bulk-call campaigns client.knowledgeBase // Knowledge base files client.phoneNumbers // Phone numbers client.providers // LLM, voice, STT, TTS providers client.integrations // Integrations and agent attachment client.reseller // Partner operations (partner credentials required) ``` " title="Agents" href="/docs/api-reference/agents/listAgents" description="Create, retrieve, update, and delete AI voice agents." /> " title="Calls" href="/docs/api-reference/calls/dispatchCall" description="Dispatch calls and read call logs and transcripts." /> " title="Knowledge base" href="/docs/api-reference/knowledge-base/listKnowledgeBaseFiles" description="Upload, attach, and manage knowledge base files." /> " title="Phone numbers" href="/docs/api-reference/phone-numbers/listPhoneNumbers" description="List, attach, and import phone numbers." /> ## Error handling [#error-handling] Non-2xx responses and network failures throw `OmniDimensionError`: ```ts import OmniDimension, { OmniDimensionError } from '@omnidim-ai/sdk'; const client = new OmniDimension({ apiKey: process.env.OMNIDIM_API_KEY! }); try { await client.agents.get(999); } catch (err) { if (err instanceof OmniDimensionError) { console.error(err.status, err.message, err.body); } } ``` `status` is the HTTP status code (0 for network or timeout errors). `body` is the parsed error payload when present. ## Types [#types] Schema types are exported for use in your own code: ```ts import type { Agent, Call, Voice } from '@omnidim-ai/sdk'; ``` ## Client options [#client-options] ```ts interface ClientOptions { // Your OmniDimension API key. Required. apiKey: string; // Override the API base URL. Defaults to production. baseURL?: string; // Per-request timeout in milliseconds. Defaults to 60000. timeout?: number; // Custom fetch implementation. Defaults to the global fetch. fetch?: typeof fetch; // Headers added to every request. defaultHeaders?: Record; } ``` # Web widget (/docs/sdks/web-widget) > Embed your agent in the browser with a no-code script. The web widget is the no-code way to run an agent in the browser. It loads from a copy-paste embed script and authenticates with a public, agent-scoped widget key that is safe to appear in page source. This widget key is distinct from your server-side API secret key, which must never ship to the browser. Use it to put a floating voice or chat experience on your website. Want the conversation inside your own UI instead? Use the [web SDK](/docs/sdks/web). Use a [Server SDK](/docs/sdks) when you need to manage agents, dispatch calls, or read data from your backend. ## Deploy it [#deploy-it] The widget is generated and configured from the dashboard. The full walkthrough, including customization and the embed snippet, is in the deployment guide: - [Web chat widget guide](/docs/dashboard-guides/web-chat-widget) — Generate, customize, and embed the widget on your site. # Web SDK (/docs/sdks/web) > The @omnidim-ai/client package. Put a live voice conversation with your agent on any page with the WebSession class. `@omnidim-ai/client` is the browser SDK for web call sessions. One small class, `WebSession`, turns a session into a working voice conversation: microphone capture, agent audio playback, barge-in, and transcripts are all handled for you. It pairs with a server-side call to create the session: your backend holds the API key, the page only ever receives a short-lived `ws_url`. Create sessions on your server. The create response's `ws_url` is the only thing the page needs, and it is single-conversation and short-lived, so page source, network traces, and logs never expose a reusable credential. ## Installation [#installation] ```bash npm install @omnidim-ai/client ``` No bundler? A script tag works too and exposes the global `OmnidimensionClient`: ```html ``` ## Quickstart [#quickstart] ### 1. Create a session on your server [#1-create-a-session-on-your-server] ```bash curl -X POST https://omnidim.io/api/v1/sessions/create \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{"agent_id": 158910, "type": "voice", "custom_variables": {"name": "Demo User"}}' ``` ```js const response = await fetch('https://omnidim.io/api/v1/sessions/create', { method: 'POST', headers: { Authorization: `Bearer ${process.env.OMNIDIM_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ agent_id: 158910, type: 'voice', custom_variables: { name: 'Demo User' }, }), }); const { ws_url } = await response.json(); ``` The response contains `session_id`, `token`, `expires_at`, and the `ws_url` your page connects to. See [create session](/docs/api-reference/sessions/createSession) for the full contract, including the balance and concurrency errors. ### 2. Start the conversation in the browser [#2-start-the-conversation-in-the-browser] ```js import { WebSession } from '@omnidim-ai/client'; const session = new WebSession(); session.on('status', (s) => console.log('status', s)); session.on('transcript', (t) => console.log(t.role, t.text)); session.on('error', (e) => console.error(e)); await session.start({ wsUrl }); // asks for mic permission, then talks ``` That is the whole integration: the visitor talks, the agent answers, and interrupting the agent mid-sentence stops its audio immediately. ## API [#api] ### `session.start({ wsUrl })` [#sessionstart-wsurl-] Requests microphone permission, connects, and resolves once the conversation is live. Audio is echo-cancelled and noise-suppressed. ### `session.mute(muted)` [#sessionmutemuted] `true` stops sending microphone audio, `false` resumes. The stream stays warm, so unmuting never re-prompts for permission. ### `session.stop()` [#sessionstop] Hangs up and releases the microphone. ### Events [#events] | Event | Payload | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `'connecting'`, `'active'`, or `{ state: 'ended', reason }`. Reasons include `hangup`, `stopped`, `insufficient_balance`, and `connection_lost`. | | `transcript` | `{ role: 'user' \| 'agent', text, final }`. For both roles, `text` is a cumulative snapshot of the current turn (render the latest, do not append); `final: false` is interim, `final: true` is settled. | | `error` | An `Error`. The conversation may still continue. | If the connection drops mid-call, the SDK reconnects once automatically and the session re-attaches to the same conversation. ## Integrating without the SDK [#integrating-without-the-sdk] On platforms without an official SDK (iOS, Android, Flutter, kiosks), speak the WebSocket protocol directly. It is small: one frame type to send, a handful of events to receive. See the [web call protocol](/docs/web-call-protocol). # Import Exotel Phone Numbers (/docs/telephony/exotel-import) > Import your Exotel phone numbers into OmniDimension and set up call flows for your AI agents. This guide walks through importing Exotel phone numbers into OmniDimension so your AI agents can handle incoming calls. You'll set up the necessary call flows in Exotel and import them into OmniDimension. ## What you'll need [#what-youll-need] Before you can import an Exotel phone number, you need to complete these steps in your Exotel account: * Create an account on [exotel.com](https://exotel.com) * Complete KYC (Know Your Customer) verification by submitting required documents * Purchase an Exophone number from your Exotel dashboard * Create a call flow for your Exophone number (covered in step 4) * Attach the call flow to your phone number ## Required credentials from Exotel [#required-credentials-from-exotel] After completing the prerequisites, you'll need to collect these credentials from your Exotel dashboard. You can find these in the **API Credentials** section: * **Exotel API Key**: Your authentication key for API access * **Exotel API Token**: Your secure access token * **Exotel Subdomain**: Your account's subdomain (e.g., `yourcompany.exotel.com`) * **Exotel Account SID**: Your unique account identifier * **Exotel Phone Number**: The phone number you want to import * **Exotel App ID**: The call flow ID (you'll get this after creating your call flow) ## Setting up the call flow in Exotel [#setting-up-the-call-flow-in-exotel] This is the most important part of the process. You'll create a call flow in Exotel that connects to OmniDimension. Follow these steps carefully. ### Access App Builder [#access-app-builder] In your Exotel dashboard, go to the side menu and click on **App Builder**. Navigate to App Builder ### Create new app [#create-new-app] Click on **Create New App** to start building your call flow. Create New App ### Add Voicebot applet for "Call Comes" [#add-voicebot-applet-for-call-comes] On the **Call Comes** block, drag a **Voicebot** applet from the right sidebar. In the URL field, enter `wss://live.omnidim.io/media_exotel_community` and turn recording **ON**. Add Voicebot Applet ### Add second Passthru applet [#add-second-passthru-applet] On the voicebot applet's next section, drag another **Passthru** applet. In the URL field, enter `https://live.omnidim.io/should_end_the_call`. Add Second Passthru ### Add Hangup on 200 response [#add-hangup-on-200-response] In the 200 response of the second passthru applet, drag a **Hangup** applet to end the call when needed. Add Hangup on 200 ### Add Connect on 302 response [#add-connect-on-302-response] In the 302 response of the second passthru applet, drag a **Connect** applet for call transfers. Add Connect on 302 ### Configure Connect applet [#configure-connect-applet] In the connect applet, select **Primary URL** and enter `https://live.omnidim.io/call_transfer`. Configure Connect Applet ### Add Passthru for call end [#add-passthru-for-call-end] For the **After the call conversation ends** option, add a passthru applet with URL `https://live.omnidim.io/should_end_the_call?omni_status=call_end`. Add Passthru for Call End ### Add Passthru for no answer [#add-passthru-for-no-answer] For the **If nobody answers** option, add a passthru applet with URL `https://live.omnidim.io/should_end_the_call?omni_status=nobody_ans`. Add Passthru for No Answer ### Add Passthru for no dial [#add-passthru-for-no-dial] For the **We didn't dial anyone** option, add a passthru applet with URL `https://live.omnidim.io/should_end_the_call?omni_status=didnt_dial_anyone`. Add Passthru for No Dial ### Add Hangup applets [#add-hangup-applets] On all the passthru applets you just created, add a **Hangup** applet in both 200 and 302 responses to ensure calls end properly. Add Hangup Applets ### Save your call flow [#save-your-call-flow] Click **Save** to save your call flow configuration and close the flow builder. Make sure to note down the App ID (call flow ID) as you'll need it for the import process. Save Call Flow ### Attach call flow with your Exophone number [#attach-call-flow-with-your-exophone-number] Go to **Exophone**, buy a number, and attach the call flow you just created. Make sure to note down the phone number as you'll need it for the import process. Attach Call Flow ## Important notes about call flow setup [#important-notes-about-call-flow-setup] * The Voicebot applet will only be visible after you complete KYC verification * If you don't see the Voicebot applet after KYC, contact Exotel support * Make sure all URLs are entered exactly as shown (no extra spaces or characters) * Test your call flow in Exotel before importing to OmniDimension * Save your App ID (call flow ID); you'll need it for the import process ## Import to OmniDimension [#import-to-omnidimension] Now that your call flow is set up in Exotel, import your phone number into OmniDimension. ### Navigate to Phone Numbers [#navigate-to-phone-numbers] In your OmniDimension dashboard, go to the **Phone Numbers** section from the main navigation menu. Phone Numbers Section ### Click "Import Exotel Phone Number" [#click-import-exotel-phone-number] Look for the **Import Exotel Phone Number** button and click on it to open the import form. Import Button ### Fill in the import form [#fill-in-the-import-form] Enter all the credentials you collected from your Exotel dashboard in the form fields: * Exotel API Key * Exotel API Token * Exotel Subdomain * Exotel Account SID * Exotel Phone Number * Exotel App ID (your call flow ID) Import Form ### Complete the import [#complete-the-import] Click the **Import** button to complete the process. Your Exotel phone number will now appear in your OmniDimension dashboard and be ready for use with your AI agents. Success Import ## Understanding the call flow [#understanding-the-call-flow] Here's how your call flow works after setup. When someone calls your Exotel number: * The call first goes to OmniDimension's `call_coming` endpoint * OmniDimension then connects to your AI agent through the voicebot * Your AI agent handles the conversation * When the call should end, it goes to the `should_end_the_call` endpoint * If a transfer is needed, it goes to the `call_transfer` endpoint * All calls eventually end with a hangup action Complete call flow overview ## What happens after import [#what-happens-after-import] Once your phone number is successfully imported, you can: * Use the phone number with any of your AI agents * Receive incoming calls that are automatically handled by your agents * Monitor call logs and performance in your dashboard ## Next steps [#next-steps] * Configure your AI agents to handle calls effectively * Attach the Exotel phone number to your agent * Test your setup by making a test call to your number ## Troubleshooting common issues [#troubleshooting-common-issues] * **Voicebot applet not visible**: Make sure you've completed KYC verification. If still not visible, contact Exotel support. * **Import fails**: Double-check all credentials are correct and your call flow is properly configured. * **Calls not connecting**: Verify your call flow URLs are correct and your OmniDimension account is active. * **Call hangup on connect**: Make sure all URLs are entered exactly as shown with no extra spaces. * **KYC issues**: Contact Exotel support if you're having trouble with the verification process. ## Getting help [#getting-help] * [Exotel Documentation](https://developer.exotel.com/applet?utm_source=omnidim\&utm_medium=docs#voicebot) * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # Bring your own number (/docs/telephony) > Import phone numbers you already own into OmniDimension, from Twilio, any SIP carrier, or Exotel. Already have a number with another provider? Import it here. If you don't have one yet, [buying a number from OmniDimension](/docs/dashboard-guides/numbers-shop) is faster. See [Get a phone number](/docs/get-a-number) for a side-by-side of every option. ## Import from Twilio [#import-from-twilio] The simplest import if you're on Twilio. Paste your account SID and auth token, and your number is ready. - [Import a Twilio number](/docs/telephony/twilio-import) — Direct import with your Twilio SID and token. No SIP trunk setup. ## Import via SIP [#import-via-sip] OmniDimension speaks **SIP**, the standard protocol every modern carrier supports. If your provider can hand out SIP trunk credentials, you can plug it in. Any SIP carrier works the same way. - [SIP Trunking (any carrier)](/docs/telephony/sip) — The universal flow. Works with every SIP-enabled provider: paste credentials, point a number, done. ### Carrier-specific quick starts [#carrier-specific-quick-starts] Use these if your carrier is on the list. They skip the lookup steps and go straight to the fields you need. - [RingCentral SIP](/docs/telephony/sip-ringcentral) — RingCentral SIP configuration with optional outbound proxy. - [Vonage SIP](/docs/telephony/sip-vonage) — Vonage SIP trunking with UserKey and Secret authentication. - [Twilio via SIP (advanced)](/docs/telephony/sip-twilio) — Elastic SIP Trunking with credential lists or IP ACL. Only if you need a trunk. ## Import from Exotel [#import-from-exotel] - [Import an Exotel number](/docs/telephony/exotel-import) — Import Exotel phone numbers and configure end-to-end call flows for your AI agents. # RingCentral SIP Configuration (/docs/telephony/sip-ringcentral) > Complete guide to configure your RingCentral SIP trunking, including SIP credentials and optional outbound proxy. RingCentral's SIP trunking allows you to connect your existing phone numbers to OmniDimension AI agents. This guide covers the complete setup process including SIP credentials and optional outbound proxy configuration. ## Prerequisites [#prerequisites] ### What you'll need [#what-youll-need] * Phone number with SIP capability * SIP trunking enabled on your account ### Configuration features [#configuration-features] * Optional outbound proxy support * IP whitelisting available ## Step-by-step configuration [#step-by-step-configuration] ### Enable SIP trunking [#enable-sip-trunking] First, you need to enable SIP trunking in your RingCentral account. 1. Go to the [RingCentral Admin Portal](https://service.ringcentral.com) 2. Sign in with your admin credentials 3. Navigate to **Phone System → SIP Trunking** 4. Enable SIP trunking for your account if not already enabled ### Get your SIP configuration [#get-your-sip-configuration] Configure your SIP trunk in RingCentral and copy the configuration details. 1. Go to **Phone System → Phones & Devices** 2. Click **Set up manually using SIP** 3. Configure your SIP credentials and settings 4. If using outbound proxy, choose the one closest to your region (e.g., `sip60.ringcentral.com:5090` for US) 5. Copy your complete SIP configuration details (host, username, password, etc.) ### Import to OmniDimension [#import-to-omnidimension] Now import your RingCentral number to OmniDimension. 1. Go to **Phone Numbers** on the OmniDimension dashboard 2. Click **Import from SIP Trunk** 3. Select **RingCentral** as your provider 4. Enter your SIP credentials (host, username, password, outbound proxy if needed) 5. Test the connection and import For comprehensive setup instructions, refer to RingCentral's official [SIP Settings Guide](https://support.ringcentral.com/article-v2/Manual-Provisioning-How-get-SIP-Settings.html?brand=RC_US\&product=RingEX\&language=en_US\&utm_source=omnidim\&utm_medium=docs). # Twilio via SIP (advanced) (/docs/telephony/sip-twilio) > Connect Twilio numbers through Elastic SIP Trunking with credential lists or IP ACL authentication. For most users, the direct Twilio import is simpler. If you just want to use a Twilio number with your agents, the [direct Twilio import](/docs/telephony/twilio-import) takes only your account SID and auth token. Use this SIP guide only if you specifically need Elastic SIP Trunking. Twilio's Elastic SIP Trunking allows you to connect your existing phone numbers to OmniDimension AI agents. This guide covers both credential-based and IP whitelisting authentication methods. ## Prerequisites [#prerequisites] ### What you'll need [#what-youll-need] * Active Twilio account * Elastic SIP Trunking enabled * Phone numbers associated with your trunk ### Authentication options [#authentication-options] * Custom SIP credentials (recommended) * IP Access Control Lists (ACL) * Both methods supported ## Step-by-step configuration [#step-by-step-configuration] ### Create your Elastic SIP Trunk [#create-your-elastic-sip-trunk] First, you need to create an Elastic SIP Trunk in your Twilio Console. 1. Go to the [Twilio Console](https://www.twilio.com/en-us/sip-trunking?utm_source=omnidim\&utm_medium=docs) and navigate to **Elastic SIP Trunking → Manage → Trunks** 2. Click **Create New Trunk** 3. Enter a friendly name for your trunk (e.g., "OmniDimension Trunk") 4. Configure your trunk settings and save ### Configure termination settings [#configure-termination-settings] Configure how your trunk authenticates with OmniDimension. You have two options: #### Option 1: Credential Lists (recommended) [#option-1-credential-lists-recommended] Works without IP whitelisting. 1. Go to **Credential Lists** in your trunk 2. Create a new credential list 3. Add username / password pairs 4. Assign to your trunk #### Option 2: IP ACL [#option-2-ip-acl] Requires IP whitelisting. 1. Enable **Access Control List** 2. Create IP ACL 3. Add OmniDimension IPs (contact us for the specific IP addresses) 4. Assign to your trunk ### Associate phone numbers [#associate-phone-numbers] Link your Twilio phone numbers to the SIP trunk. 1. Go to **Phone Numbers → Manage → Active numbers** 2. Select the number you want to use 3. In the Voice section, set **Configure with** to **SIP Trunk** 4. Select your newly created trunk 5. Save the configuration ### Import to OmniDimension [#import-to-omnidimension] Now import your Twilio number to OmniDimension. 1. Go to **Phone Numbers** on the OmniDimension dashboard 2. Click **Import from SIP Trunk** 3. Select **Twilio** as your provider 4. Enter your SIP credentials 5. Test the connection and import For comprehensive setup instructions, refer to Twilio's official [Elastic SIP Trunking Setup Guide](https://www.twilio.com/en-us/blog/elastic-sip-trunking-step-by-step-setup?utm_source=omnidim\&utm_medium=docs). # Voicelink SIP Setup (/docs/telephony/sip-voicelink) > Connect a Voicelink DID to your OmniDimension agents over SIP. The host, port, and dial prefix are filled in for you, so you only pick Voicelink and enter your number. Voicelink connects to OmniDimension over a shared SIP trunk. You set your trunk and DID to route through OmniDimension in the Voicelink panel, then import the number on the OmniDimension dashboard. The connection details (host, port, and dial prefix) are applied automatically when you pick Voicelink, so there is nothing to copy by hand. ## Prerequisites [#prerequisites] * A Voicelink account with a DID and a SIP trunk * Access to the Voicelink admin panel * The DID number you want to use with your agent ## Step-by-step configuration [#step-by-step-configuration] ### Set the trunk to OmniDimension [#set-the-trunk-to-omnidimension] In the Voicelink panel, open **Voice Services → SIP Trunk Management** and edit your trunk. Under **Bot Configuration**: * **BOT Provider**: select **OmniDimension** * **Status**: set to **Active** * Leave **Peer Monitoring** enabled Voicelink SIP trunk Bot Configuration set to OmniDimension ### Route your DID through the trunk [#route-your-did-through-the-trunk] On the same trunk page, under **Call Routing Configuration**, add your DID to **Inbound Call - DIDs** (and to the outbound list, or turn on **Use the same DIDs for outbound calls**). Voicelink trunk call routing with a DID selected for inbound and outbound ### Configure the DID routing [#configure-the-did-routing] Open **Voice Services → DID Call Routing**, edit your DID, and set: * **Status**: **Active** * **Inbound Call → Route Call**: **SIP trunk**, then select your trunk * **Outbound Call → Route Call**: **Only answer** Voicelink DID routing with SIP trunk inbound and Only answer outbound ### Import the number to OmniDimension [#import-the-number-to-omnidimension] On the OmniDimension dashboard: 1. Go to **Phone Numbers** 2. Click **Import from SIP Trunk** 3. Select **Voicelink** as your provider 4. Enter a name and your Voicelink DID number 5. Click **Import Number** The host, port, and dial prefix are set for you. You do not need to enter SIP credentials. Voicelink authenticates by our server's IP, so there is no username or password to enter. If your number does not connect after import, recheck that the trunk and DID are both **Active** in the Voicelink panel. # Vonage SIP Setup (/docs/telephony/sip-vonage) > Complete guide to configure your Vonage SIP trunking with UserKey and Secret authentication. Vonage's SIP trunking allows you to connect your existing phone numbers to OmniDimension AI agents. This guide covers the complete setup process including UserKey and Secret authentication. ## Prerequisites [#prerequisites] ### What you'll need [#what-youll-need] * SIP trunking enabled on your account * Phone number with SIP capability ### Authentication method [#authentication-method] * UserKey and Secret authentication * IP whitelisting available ## Step-by-step configuration [#step-by-step-configuration] ### Get your SIP credentials [#get-your-sip-credentials] Access your Vonage SIP configuration to get the credentials you'll need. 1. Go to the [Vonage Admin Portal](https://admin.vonage.com) and navigate to **Phone System → SIP Trunking** 2. Find your **UserKey** (username) and **Secret** (password) 3. Note your SIP host (choose the appropriate regional host for your location) ### Import to OmniDimension [#import-to-omnidimension] Now import your Vonage number to OmniDimension. 1. Go to **Phone Numbers** on the OmniDimension dashboard 2. Click **Import from SIP Trunk** 3. Select **Vonage** as your provider 4. Enter your SIP credentials 5. Test the connection and import For comprehensive setup instructions, refer to Vonage's official [SIP Trunking Guide](https://www.vonage.com/communications-apis/sip-trunking/?utm_source=omnidim\&utm_medium=docs). # SIP Trunking (/docs/telephony/sip) > Connect your existing phone numbers to OmniDimension using SIP trunking. SIP (Session Initiation Protocol) trunking lets you use phone numbers you already own with your OmniDimension AI agents. You point your carrier at us, import the number, and it starts taking calls. This page walks through the import, the carriers we support, and how to authenticate by IP. ## Getting started [#getting-started] Importing a SIP number takes a few clicks. Before you start, make sure you have: * A SIP-enabled phone number from your carrier * Your SIP credentials (username and password), or a carrier that authenticates [by IP](#ip-based-authentication) * Access to your carrier's configuration panel ### Open the import dialog [#open-the-import-dialog] On your **Phone Numbers** page, click **Import from SIP Trunk**. SIP Import Button Location ### Enter your provider details [#enter-your-provider-details] Pick your provider from the dropdown and enter your credentials. For a carrier that isn't listed, choose **Other / Unknown** and enter your SIP host and credentials by hand. SIP Credentials Form ### Import and go live [#import-and-go-live] Submit the form. For the guided providers, the connection is configured for you and the number is ready to take calls right away. ## Supported providers [#supported-providers] You can bring a number from any SIP carrier. These four have guided imports with a step-by-step page for each: - [Twilio](/docs/telephony/sip-twilio) — Elastic SIP Trunking with credential lists or IP ACL authentication. - [RingCentral](/docs/telephony/sip-ringcentral) — SIP configuration with optional outbound proxy setup. - [Vonage](/docs/telephony/sip-vonage) — SIP trunking with UserKey / Secret authentication and regional hosts. - [Voicelink](/docs/telephony/sip-voicelink) — Shared-trunk setup with host, port, and dial prefix filled in. Just enter your DID, no credentials needed. You don't need us to add your carrier first. Choose **Other / Unknown** in the import dialog and enter your SIP host and credentials (or use [IP-based authentication](#ip-based-authentication) if your carrier authenticates by IP). Most carriers connect on the first try. Still stuck after importing? Reach out and we'll help: * Email: [support@omnidim.io](mailto:support@omnidim.io) * Discord: [join the OmniDimension community](https://discord.gg/kdjzykMTHJ) ## IP-based authentication [#ip-based-authentication] Some carriers authenticate by IP address instead of a username and password. If yours does, allow OmniDimension's SIP endpoint so your trunk accepts our calls without credentials. * **Whitelist this host:** `sip.omnidim.io` * **If your carrier's access list only accepts a literal IP** (Twilio's IP ACL, for example), resolve the host and enter the address it returns: ```bash dig +short sip.omnidim.io ``` ## Troubleshooting [#troubleshooting] ### The import didn't go through [#the-import-didnt-go-through] * Double-check your SIP credentials and confirm your carrier allows outbound calls * Check your account balance and that your plan includes SIP trunking ### The number imported but calls don't connect [#the-number-imported-but-calls-dont-connect] * Verify your SIP trunk settings and that your plan covers outbound calls to your target regions * Review the call logs in your carrier's dashboard, and confirm SIP trunking is enabled on their side ## FAQs [#faqs] SIP calls use your carrier's rates. You'll only pay for OmniDimension usage. Yes. You can import numbers from different carriers, and each number keeps its own configuration. For the guided providers (Twilio, RingCentral, Vonage), entering your credentials is all it takes. Voicelink needs its trunk and DID pointed at OmniDimension in the Voicelink panel first. For any other carrier, you enter your SIP host and credentials yourself when importing. Currently you'll need to delete and re-import the number to change its SIP settings. In-place editing is on the way. Yes. See the [Import SIP Trunk API reference](/docs/api-reference/phone-numbers) for the full endpoint and code examples. # Import a Twilio number (/docs/telephony/twilio-import) > Import an existing Twilio number into OmniDimension with your account SID and auth token. No SIP trunk setup required. If you already own a number on Twilio, this is the fastest way to use it with your AI agents. You paste your Twilio credentials and the number, and OmniDimension handles the rest. No SIP trunk configuration needed. This direct import covers most Twilio users. If you specifically need Elastic SIP Trunking (for example, to route through an existing trunk), use the [Twilio via SIP guide](/docs/telephony/sip-twilio) instead. ## What you'll need [#what-youll-need] * An active Twilio account with at least one phone number * Your Twilio **Account SID** and **Auth Token**, found on the [Twilio Console dashboard](https://www.twilio.com/console) * The phone number you want to import, in E.164 format (e.g. `+15551234567`) ## Import your number [#import-your-number] ### Open the Phone Numbers page [#open-the-phone-numbers-page] In your OmniDimension dashboard, go to **Phone Numbers**. ### Choose Twilio [#choose-twilio] Click **Bring your own number** and select **Twilio**. ### Enter your details [#enter-your-details] Fill in the form: * **Name**: a label to identify the number in your dashboard * **Phone number**: the Twilio number in E.164 format * **Account SID**: from your Twilio Console * **Auth Token**: from your Twilio Console ### Import [#import] Click **Import**. The number appears in your Phone Numbers list, ready to attach to an agent. ## Attach it to an agent [#attach-it-to-an-agent] From the **Phone Numbers** page, attach the imported number to any of your agents. Incoming calls to that number are then handled by the agent. See [Configure your agent](/docs/dashboard-guides/configure-your-agent) for setup. ## Import via API [#import-via-api] You can also import programmatically. See the [Import Twilio number API reference](/docs/api-reference/phone-numbers/importTwilioNumber) for the endpoint and code examples. ## Troubleshooting [#troubleshooting] * **Import fails**: Double-check your Account SID and Auth Token, and confirm the number is active in your Twilio account. * **Wrong number format**: Use E.164 format with the country code, e.g. `+15551234567`. * **Calls not connecting**: Make sure the number is attached to an agent and your account has sufficient balance. ## Getting help [#getting-help] * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # Tutorials (/docs/tutorials) > Video walkthroughs for building, configuring, and integrating OmniDimension voice agents. Step-by-step video guides to help you build, configure, and scale your AI voice agents with OmniDimension. Each walkthrough covers a single workflow end to end. ## Getting started [#getting-started] ## Configuration [#configuration] ## Telephony [#telephony] ## Integrations [#integrations] ## WhatsApp [#whatsapp] ## Analytics [#analytics] ## Bulk operations [#bulk-operations] ## Industry use cases [#industry-use-cases] # Import your WhatsApp Cloud number (/docs/whatsapp/import-number) > Connect a WhatsApp Cloud API number to OmniDimension using your four Meta credentials, then attach an AI agent. Once you have your four Meta credentials, importing a number into OmniDimension takes a couple of minutes. This is the recommended way to connect WhatsApp for production: the number stays connected, and your AI agent can both send and reply. ## Before you start [#before-you-start] You need the four values from [Get your Meta Cloud credentials](/docs/whatsapp/meta-credentials): * WhatsApp Business Account ID * Meta App ID * Meta Business ID * A permanent Meta Access Token If you do not have these yet, set them up first. The import will not complete without all four. ## Import the number [#import-the-number] ### Open the connect dialog [#open-the-connect-dialog] In your OmniDimension dashboard, go to **Phone Numbers**, click **Add Number**, and choose **WhatsApp Cloud Business**. Add a WhatsApp number and pick a method ### Paste your four credentials [#paste-your-four-credentials] Fill in the form with the values you collected from Meta: * **WhatsApp Business Account ID** * **Meta Access Token** (your permanent System User token) * **Meta App ID** * **Meta Business ID** Then click **Connect**. The WhatsApp Cloud credentials form in OmniDimension ### Pick the number to import [#pick-the-number-to-import] OmniDimension authenticates with Meta and fetches the phone numbers on your WhatsApp Business Account. If your account has more than one number, choose the one you want. If it has just one, OmniDimension connects it directly. The number then appears in your WhatsApp numbers list. A connected WhatsApp Cloud number in the OmniDimension numbers list ### Check send-readiness [#check-send-readiness] After connecting, OmniDimension runs a quick health check and shows whether the number is ready to send. If anything needs attention (for example, the number's display name is still pending Meta review), it is listed here so you can fix it before your first message. ### Attach an AI agent [#attach-an-ai-agent] Find the connected number and open the **Attach assistant** dropdown on its card, then pick the agent you want to handle this number. The agent's prompt and language decide how it talks to customers. Once you pick one, its name shows under **Answered by**. The Attach assistant dropdown on a connected WhatsApp number ## Turn on AI replies [#turn-on-ai-replies] Importing the number lets your agent **send** messages and run campaigns. To also let it **reply** when a customer messages you, you do a one-time activation so Meta forwards incoming messages to OmniDimension. Right after import, OmniDimension shows a **Let your AI reply to messages** step. You can do it then, or anytime later from the number's menu, where an inactive number shows an **Inactive · Set up** button. " title="Set up inbound replies" href="/docs/whatsapp/inbound-replies" description="Activate automatic AI replies in one step. Two methods, takes a minute." /> ## Common errors and what they mean [#common-errors-and-what-they-mean] If the import fails, the message usually points to one of these. Each has a quick fix. **Account not registered (error 133010).** The number is still live on the WhatsApp Business app, not the Cloud API. Add it through Meta's API Setup or migrate it off the app first. See [step 3 of the credentials guide](/docs/whatsapp/meta-credentials#add-your-own-phone-number-for-production) and Meta's [Register a business phone number](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration/?utm_source=omnidim\&utm_medium=docs). **Invalid or expired token (401 or 190).** You most likely pasted the temporary token from API Setup, which expires in 24 hours. Generate a permanent system user token instead and try again. See [step 5 of the credentials guide](/docs/whatsapp/meta-credentials#create-a-system-user-and-generate-a-permanent-token) and Meta's [access tokens guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens/?utm_source=omnidim\&utm_medium=docs). **WhatsApp account not assigned to your system user.** The token is valid but the system user does not have access to this WhatsApp account. In Business settings → System users, select the user, click **Add assets**, and assign your WhatsApp account with **Full control**. **Missing permissions.** The token was generated without `whatsapp_business_messaging` and `whatsapp_business_management`. Regenerate it with both permissions enabled. ## Getting help [#getting-help] * [Get your Meta Cloud credentials](/docs/whatsapp/meta-credentials) * [Set up inbound replies](/docs/whatsapp/inbound-replies) * Meta: [WhatsApp Cloud API get started](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started?utm_source=omnidim\&utm_medium=docs) and [error codes](https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/?utm_source=omnidim\&utm_medium=docs) * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # Inbound replies on WhatsApp (/docs/whatsapp/inbound-replies) > Let your AI agent reply automatically to messages customers send to your WhatsApp number. When a customer messages your WhatsApp number, your AI agent can reply automatically. This takes a one-time setup on your Meta account so Meta forwards incoming messages to OmniDimension. This is the last of the three Cloud stages, after [getting your credentials](/docs/whatsapp/meta-credentials) and [importing your number](/docs/whatsapp/import-number). ## Before you start [#before-you-start] * Your WhatsApp number is [imported in OmniDimension](/docs/whatsapp/import-number). * An agent is attached to the number. The agent's prompt and language decide how it replies. ## Activate inbound replies [#activate-inbound-replies] Until you do this once, a freshly imported number shows **Replies: Inactive · Set up** on its card. Your agent can still send messages and run campaigns, it just will not reply to incoming ones yet. A freshly imported WhatsApp number showing Replies as Inactive, ready to set up Right after you import a number, OmniDimension shows an "Activate replies" step. You can also start it anytime from the number card by clicking **Inactive · Set up**. Either way, you get this dialog. Pick one of the two methods. The Reply to WhatsApp messages dialog with both activation methods ### Method 1: Connect automatically (recommended) [#method-1-connect-automatically-recommended] Paste your Meta app secret and we set everything up for you. Find it in the [Meta App Dashboard](https://developers.facebook.com/apps/) under **App settings → Basic** (the same screen as your App ID; click **Show** next to App secret). ### Method 2: Set it up yourself [#method-2-set-it-up-yourself] Prefer not to share your app secret? In the Meta App Dashboard, open WhatsApp, then Configuration (see [Meta's webhooks guide](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks?utm_source=omnidim\&utm_medium=docs) for reference): WhatsApp webhook configuration in the Meta App Dashboard 1. Next to **Webhook**, click **Edit**. Paste the **Callback URL** and **Verify token** shown in OmniDimension, then click **Verify and save**. 2. Next to **Webhook fields**, click **Manage**. In the popup that appears, subscribe to the messages field, then click **Done**. Meta verifies the callback the moment you click Verify and save. Once it is verified and **messages** is subscribed, replies start working. Subscribing to the **messages** field is required. Setting the callback URL on its own will not deliver any messages, this is the step people miss most often. ## Good to know [#good-to-know] * Replies use the agent attached to the number. Change how it answers by editing that agent's prompt and language. * The reply is sent from your own WhatsApp number through your Meta account. * Image and other media messages are handled separately, and support for them is expanding. ## Getting help [#getting-help] * [Get your Meta Cloud credentials](/docs/whatsapp/meta-credentials) * [Import your WhatsApp Cloud number](/docs/whatsapp/import-number) * Meta: [Set up webhooks](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks?utm_source=omnidim\&utm_medium=docs) and [App settings → Basic](https://developers.facebook.com/apps/) * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # WhatsApp Integration (/docs/whatsapp) > Connect your WhatsApp number to OmniDimension so an AI agent can send messages, run campaigns, and reply to customers automatically. Connect a WhatsApp number to OmniDimension and an AI agent can send messages, run campaigns, and reply to incoming customer messages automatically. ## Ways to connect [#ways-to-connect] You add a WhatsApp number from the **Add Number** button. Pick the method that matches how you use WhatsApp. Add a WhatsApp number and pick a method | Method | Best for | Notes | | --------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------- | | **WhatsApp Cloud Business** | Production (recommended) | Import via the Meta Cloud API. Stable, sends and replies, and supports campaigns. | | **Phone WhatsApp** | A quick test | Link an existing number by QR code. The session expires after about 48 hours and needs re-linking. | | **Import from Twilio** | A number already on Twilio | Enter your Twilio credentials, then attach an agent. | | **Import with Interakt** | A number already on Interakt | Connect with the Interakt API key, then attach an agent. | ## Set up WhatsApp Cloud (recommended) [#set-up-whatsapp-cloud-recommended] The Cloud API is the production path. There are three short stages: get your credentials from Meta, import the number, then turn on AI replies. " title="1. Get your Meta credentials" href="/docs/whatsapp/meta-credentials" description="Find the four values Meta gives you, and set up your Meta account from scratch if you are starting fresh." /> " title="2. Import your number" href="/docs/whatsapp/import-number" description="Paste the four credentials into OmniDimension, pick your number, and attach an AI agent." /> " title="3. Turn on AI replies" href="/docs/whatsapp/inbound-replies" description="A one-time activation so Meta forwards incoming messages and your agent can reply." /> ## Phone WhatsApp (quick test) [#phone-whatsapp-quick-test] Want to try WhatsApp in a couple of minutes without any Meta setup? Link an existing number by QR code. This is good for testing, not production: the linked session expires after about 48 hours and has to be re-linked. ### Start the connection [#start-the-connection] From **Add Number**, choose **Phone WhatsApp**. ### Scan the QR code [#scan-the-qr-code] A QR code appears on screen. On your phone: 1. Open **WhatsApp**. 2. Go to **Settings** (or **Menu** on Android). 3. Tap **Linked Devices**, then **Link a Device**. 4. Point your camera at the QR code. Scan the WhatsApp QR code The QR code is valid for about **15 seconds**. If it expires, regenerate it and scan again. ### Attach an agent [#attach-an-agent] Once linked, your number appears in the list. Click **Attach Assistant** on its card and pick an agent. Any message to that number is now handled automatically. Attaching an agent to the WhatsApp number ## Twilio and Interakt [#twilio-and-interakt] If you already run WhatsApp through Twilio or Interakt, choose that method under **Add Number**, enter the provider's credentials, and attach an agent. The flow is the same shape as the Cloud import above. ## Pricing [#pricing] * WhatsApp is included on the Jump Starter plan and above. Accounts created before feature tiers were introduced keep the access they already had. * Every outgoing WhatsApp message, whether an agent reply or a campaign message, is billed from your wallet balance at a flat per-message rate, currently $0.006. Incoming messages are free. * Meta bills its own per-message charges separately to your WhatsApp Business account. Those are not included in the OmniDimension rate. For current rates, check the live pricing page at [omnidim.io/pricing](https://omnidim.io/pricing). ## Getting help [#getting-help] * Meta: [WhatsApp Cloud API get started](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started?utm_source=omnidim\&utm_medium=docs) and [Meta App Dashboard](https://developers.facebook.com/apps/) * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # Get your Meta Cloud credentials (/docs/whatsapp/meta-credentials) > Find the four credentials OmniDimension asks for when you import a WhatsApp Cloud number, and how to set up your Meta account from scratch. To import a WhatsApp number with the Meta Cloud API, OmniDimension asks for four values from your Meta account. This page shows you exactly where each one lives, and how to set up the Meta account from scratch if you are starting fresh. Set aside about 20 minutes the first time. You only do this once per number. ## The four credentials [#the-four-credentials] These are the four fields on the **WhatsApp Cloud Business** import form, and where each one comes from. | Credential | What it is | Where to find it | | -------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- | | **WhatsApp Business Account ID** | A 15-16 digit number identifying your WhatsApp account | Business settings → WhatsApp accounts (or WhatsApp → API Setup) | | **Meta App ID** | A 15-16 digit number identifying the app you created | App settings → Basic (also on API Setup) | | **Meta Business ID** | A 15-16 digit number identifying your business portfolio | Business settings → Business portfolio info | | **Meta Access Token** | A long string starting with `EAA…` that lets OmniDimension send on your behalf | Your system user (permanent token) | Three of the four are just ID numbers you copy. The only one that takes real setup is the access token, covered in [step 5](#create-a-system-user-and-generate-a-permanent-token). If your Meta account already has an app with WhatsApp added, skip to [Where each credential lives](#where-each-credential-lives). ## Set up Meta from scratch [#set-up-meta-from-scratch] If you have never used the WhatsApp Cloud API before, do these five steps in order. If you already have a Meta app with WhatsApp added, jump to [step 4](#find-your-business-portfolio-id). ### Create a Meta Business portfolio [#create-a-meta-business-portfolio] A business portfolio (formerly "Business Manager") is the container that owns your app and WhatsApp account. 1. Go to [business.facebook.com](https://business.facebook.com/) and sign in with your Facebook account. 2. If you do not have one yet, open [Business settings](https://business.facebook.com/settings) and click **Create account**, then enter your business name, your name, and a work email. This is also where your [Meta Business ID](#find-your-business-portfolio-id) comes from later. To raise your messaging limits later, you will also complete [Meta Business verification](https://www.facebook.com/business/help/2058515294227817), but you do not need it to get your credentials or send your first test message. Meta reference: [Business portfolios](https://www.facebook.com/business/help/1710077379203657) and [Get started with the WhatsApp Cloud API](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started?utm_source=omnidim\&utm_medium=docs). ### Create an app with WhatsApp [#create-an-app-with-whatsapp] 1. Go to the [Meta App Dashboard](https://developers.facebook.com/apps/) and click [**Create app**](https://developers.facebook.com/apps/create/). 2. For the use case, choose **Other**, then select the **Business** app type. 3. Give the app a name, pick the business portfolio you just created, and click **Create app**. 4. On the app's dashboard, find **WhatsApp** in the product list and click **Set up**. Meta creates a free test number and a sandbox WhatsApp Business Account for you automatically. That is enough to get your credentials and test sending. Meta reference: [Create an app](https://developers.facebook.com/docs/development/create-an-app/) and the [WhatsApp Cloud API get started guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started?utm_source=omnidim\&utm_medium=docs). ### Add your own phone number (for production) [#add-your-own-phone-number-for-production] The test number can only message a short list of recipients you add by hand. To message real customers you add your own number: 1. In the App Dashboard, open **WhatsApp → API Setup**. 2. Under **Send and receive messages**, click **Add phone number**. 3. Enter a number that is **not** already active on the WhatsApp or WhatsApp Business app, choose how to receive the verification code (SMS or call), and verify it. Meta walks through this in [Add a phone number](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started/add-a-phone-number/?utm_source=omnidim\&utm_medium=docs), and the full registration flow is in [Register a business phone number](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration/?utm_source=omnidim\&utm_medium=docs). A number already in use on the consumer WhatsApp app or the WhatsApp Business app cannot be added directly. You either use a fresh number, or migrate the existing one off the app first. If your import later fails with "account not registered" (error `133010`), the number is still on the Business app, not the Cloud API. ### Find your business portfolio ID [#find-your-business-portfolio-id] This is the value OmniDimension calls **Meta Business ID**. 1. Go to [Settings](https://business.facebook.com/latest/settings/) in Meta Business Suite. 2. Select **Business portfolio info** from the left menu. 3. Below your business portfolio name, you'll find your **business portfolio ID** number. Copy it. Older accounts may label this "Business Manager ID." It is the same number. ### Create a system user and generate a permanent token [#create-a-system-user-and-generate-a-permanent-token] The temporary token in **API Setup** expires quickly and is not suitable for production. Create a **system user** and generate a permanent token instead. 1. Navigate to [Business settings](https://business.facebook.com/latest/settings) and click **System users** in the sidebar. 2. Click the **Add** button in the upper-right corner and follow the prompts to create a new system user. 3. Select the new system user and click **Assign assets**: * Select your **app** and toggle **Manage app** under Full control. * Select your **WhatsApp account** and toggle **Manage WhatsApp Business accounts** under Full control. * Click **Assign assets**. 4. Click **Generate token**, then: * Follow the prompts to generate your token. * Add these permissions: [`business_management`](https://developers.facebook.com/docs/permissions#b), [`whatsapp_business_messaging`](https://developers.facebook.com/docs/permissions#w), and [`whatsapp_business_management`](https://developers.facebook.com/docs/permissions#w). * Copy the token and store it somewhere secure. Meta shows it only once. The `whatsapp_business_*` permissions are required. Without them the token connects but cannot send, and the import will fail. The token is a password for your WhatsApp account, so treat it like one. Meta documents this exact flow in [step 5 of the WhatsApp get started guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started#step-5-create-a-system-user-and-generate-a-permanent-access-token?utm_source=omnidim\&utm_medium=docs), with more detail in [Install apps, generate, refresh, and revoke tokens](https://developers.facebook.com/docs/business-management-apis/system-users/install-apps-and-generate-tokens/?utm_source=omnidim\&utm_medium=docs). ## Where each credential lives [#where-each-credential-lives] Once your app and WhatsApp account exist, here is the fastest path to each value. ### WhatsApp Business Account ID [#whatsapp-business-account-id] In [Meta Business settings](https://business.facebook.com/latest/settings/profiles): 1. Open **Settings** from the menu on the left. 2. Under the **Accounts** dropdown, select **WhatsApp accounts**. 3. From the list on the right, click the WhatsApp Business Account you want. 4. The WABA name and the **WhatsApp Business Account ID** are shown at the top. You can also read it in the App Dashboard under **WhatsApp → API Setup**. Twilio has a clear visual walkthrough of the Business settings path in [Locating your WhatsApp Business Account (WABA) ID](https://help.twilio.com/articles/34240818556315). ### Meta App ID [#meta-app-id] Open **App settings → Basic** in the [App Dashboard](https://developers.facebook.com/apps/). The App ID is at the top of the page (and is also shown on WhatsApp → API Setup). The **App secret** is on the same screen behind a **Show** button. You only need the App secret if you later let OmniDimension activate inbound replies for you, covered in [Inbound replies](/docs/whatsapp/inbound-replies). ### Meta Business ID [#meta-business-id] Your **business portfolio ID**, from [Business settings → Business portfolio info](https://business.facebook.com/latest/settings/). See [step 4](#find-your-business-portfolio-id) above. ### Meta Access Token [#meta-access-token] Use the permanent system user token from [step 5](#create-a-system-user-and-generate-a-permanent-token). The temporary token on API Setup works for a quick test, but it expires quickly and the number will stop sending. The **Phone number ID** you see on the API Setup screen is handled automatically by OmniDimension when it fetches your numbers, so you do not need to copy it. ## Before you import: a quick checklist [#before-you-import-a-quick-checklist] You are ready to import once you have all four values copied somewhere safe: * WhatsApp Business Account ID (Business settings → WhatsApp accounts, or WhatsApp → API Setup) * Meta App ID (App settings → Basic) * Meta Business ID (Business settings → Business portfolio info) * A permanent Meta Access Token (from your system user) Next, head to [Import your WhatsApp Cloud number](/docs/whatsapp/import-number) to paste them into OmniDimension. ## Official Meta references [#official-meta-references] Every screen above lives in Meta's own tools. These are the authoritative pages from Meta: * [WhatsApp Cloud API: get started](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started?utm_source=omnidim\&utm_medium=docs) * [Meta App Dashboard](https://developers.facebook.com/apps/) and [Create an app](https://developers.facebook.com/docs/development/create-an-app/) * [Add a phone number](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started/add-a-phone-number/?utm_source=omnidim\&utm_medium=docs) and [Register a business phone number](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration/?utm_source=omnidim\&utm_medium=docs) * [Business settings → Business portfolio info](https://business.facebook.com/latest/settings/) and [System users](https://business.facebook.com/latest/settings) * [Step 5: create a system user and generate a permanent access token](https://developers.facebook.com/documentation/business-messaging/whatsapp/get-started#step-5-create-a-system-user-and-generate-a-permanent-access-token?utm_source=omnidim\&utm_medium=docs) * [System user tokens: install apps, generate, refresh, revoke](https://developers.facebook.com/docs/business-management-apis/system-users/install-apps-and-generate-tokens/?utm_source=omnidim\&utm_medium=docs) and the [permissions reference](https://developers.facebook.com/docs/permissions) * [WhatsApp access tokens guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens/?utm_source=omnidim\&utm_medium=docs) * [Find your WhatsApp Business Account (WABA) ID](https://help.twilio.com/articles/34240818556315) * [Meta Business verification](https://www.facebook.com/business/help/2058515294227817) ## Getting help [#getting-help] * [Import your WhatsApp Cloud number](/docs/whatsapp/import-number) * [OmniDimension Discord](https://discord.gg/kdjzykMTHJ) * [OmniDimension Support](mailto:support@omnidim.io) # Claude Code (/docs/mcp/connect/claude-code) > Connect OmniDimension to Claude Code in one terminal command. OAuth sign-in via the browser, then use OmniDimension tools from any Claude Code session. One command: ```bash claude mcp add --transport http omnidim https://mcp.omnidim.io/mcp --scope user ``` Then, inside Claude Code: ``` /mcp ``` A browser tab opens to the OmniDimension consent screen. Sign in, click Allow, and the connection is live. ## Verify [#verify] Back in Claude Code, ask: ``` What OmniDimension agents do I have? ``` Claude Code will call `listAgents` and report your agent list. If you see your agents, you're set. ## API key instead of OAuth [#api-key-instead-of-oauth] For headless or CI use, run the [local stdio server](/docs/mcp/local-server) with an API key from [omnidim.io/api-management](https://omnidim.io/api-management): ```bash claude mcp add omnidim-local -e OMNIDIM_API_KEY=sk_xxx -- npx -y @omnidim-ai/mcp-server ``` ## What `--scope user` does [#what---scope-user-does] `--scope user` writes the server config to your user-level `~/.claude.json` instead of the per-project file. The OmniDimension connector then shows up in every Claude Code session, regardless of which directory you started it from. Drop the flag if you only want it in the current project. ## Remove [#remove] ```bash claude mcp remove omnidim --scope user ``` This deletes the local config. To also revoke the OAuth token on the server side, remove the corresponding credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Claude Desktop (/docs/mcp/connect/claude-desktop) > Add OmniDimension as a custom connector in Claude Desktop or Claude.ai web. Paste one URL, sign in, and Claude can drive your voice agents directly. Both Claude Desktop and Claude.ai web use the same Connectors UI. The flow is identical on either. ### Open Connectors [#open-connectors] **Claude Desktop:** Settings → Connectors → Add custom connector. **Claude.ai:** Settings → Connectors → Add custom connector. ### Paste the URL [#paste-the-url] ``` https://mcp.omnidim.io/mcp ``` Give it a name like "OmniDimension" so it's easy to find later. ### Sign in [#sign-in] Claude opens the OmniDimension consent screen in your browser. Sign in (or sign up), then click Allow. ### Verify [#verify] In any conversation, ask "What OmniDimension agents do I have?". Claude will call `listAgents` and report your agent list. If you see your agents, you're set. From here you can ask for anything the tools cover, like "place a test call from my main agent to +15551234567". ## Plan requirements [#plan-requirements] Custom connectors are available on paid Claude.ai plans. Check the current plan documentation in your Claude.ai account for exact availability across Pro, Team, and Enterprise. ## Remove [#remove] In the same Connectors panel, remove the OmniDimension entry. To fully revoke server-side access, also remove the corresponding credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Codex (/docs/mcp/connect/codex) > Connect OmniDimension to Codex in one terminal command. OAuth sign-in via the browser, shared config across the Codex CLI, IDE extension, and the ChatGPT desktop app. One command: ```bash codex mcp add omnidim --url https://mcp.omnidim.io/mcp ``` Codex detects that the server supports OAuth and starts the sign-in flow immediately. A browser tab opens to the OmniDimension consent screen. Sign in, click Allow, and the terminal prints `Successfully logged in.` ## Verify [#verify] ```bash codex mcp list ``` The `omnidim` row shows `enabled` with `OAuth` in the Auth column. Then, inside a Codex session, ask: ``` What OmniDimension agents do I have? ``` Codex will call `listAgents` and report your agent list. If you see your agents, you're set. ## ChatGPT desktop app and IDE extension [#chatgpt-desktop-app-and-ide-extension] The Codex CLI, the Codex IDE extension, and the ChatGPT desktop app all read the same config at `~/.codex/config.toml`, so the one-command setup above covers all three. Configure once in the terminal and the connector shows up everywhere. To add it from the desktop app UI instead: 1. Open **Settings**, then **MCP servers**. 2. Select **Add server**. 3. Name it `omnidim`, choose **Streamable HTTP**, and enter `https://mcp.omnidim.io/mcp`. 4. Save, then select **Restart**. 5. If the server list shows it needs sign-in, select **Authenticate** to open the OmniDimension consent screen. ## Config file [#config-file] `codex mcp add` writes this block to `~/.codex/config.toml`. You can also add it by hand: ```toml [mcp_servers.omnidim] url = "https://mcp.omnidim.io/mcp" ``` To scope the server to a single project instead of your whole machine, put the same block in a `.codex/config.toml` inside the project (trusted projects only). ## Re-authenticate [#re-authenticate] If the token expires or you signed in to the wrong account, run the OAuth flow again: ```bash codex mcp login omnidim ``` ## API key instead of OAuth [#api-key-instead-of-oauth] For headless or CI use, run the [local stdio server](/docs/mcp/local-server) with an API key from [omnidim.io/api-management](https://omnidim.io/api-management): ```bash codex mcp add omnidim-local --env OMNIDIM_API_KEY=sk_xxx -- npx -y @omnidim-ai/mcp-server ``` Note that the npm package's `setup` command does not auto-configure Codex; use the command above. ## Remove [#remove] ```bash codex mcp remove omnidim ``` This deletes the local config. To also revoke the OAuth token on the server side, remove the corresponding credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Cursor (/docs/mcp/connect/cursor) > Add OmniDimension to Cursor by editing ~/.cursor/mcp.json. OAuth sign-in on first tool call, no API key handling. Edit `~/.cursor/mcp.json` (create the file if it does not exist): ```json { "mcpServers": { "omnidim": { "url": "https://mcp.omnidim.io/mcp" } } } ``` Restart Cursor. ## Verify [#verify] In Cursor's chat panel, ask: ``` What OmniDimension agents do I have? ``` The first time you do this, Cursor opens the OmniDimension consent screen in your browser. Approve, return to Cursor, and the tool call completes. Cursor will call `listAgents` and report your agent list. If you see your agents, you're set. ## API key instead of OAuth [#api-key-instead-of-oauth] For headless or CI use, run the [local stdio server](/docs/mcp/local-server) with an API key from [omnidim.io/api-management](https://omnidim.io/api-management) instead. The config block for Cursor is on that page. ## Remove [#remove] Delete the `omnidim` entry from `~/.cursor/mcp.json` and restart Cursor. To revoke server-side access, remove the credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Overview (/docs/mcp/connect) > One connector for every MCP client. Add OmniDimension as a custom server in Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, or Codex. Browser sign-in on the first call. The hosted MCP server lives at: ``` https://mcp.omnidim.io/mcp ``` Paste that anywhere a client asks for a custom MCP server. The first time you connect, the client opens a sign-in in your browser. Approve, and you're done. ## Pick your client [#pick-your-client] " title="Claude Code" href="/docs/mcp/connect/claude-code" description="One terminal command." /> " title="Claude Desktop" href="/docs/mcp/connect/claude-desktop" description="Settings, then Connectors. Same flow on Claude.ai." /> " title="Cursor" href="/docs/mcp/connect/cursor" description="Edit one JSON file." /> " title="VS Code" href="/docs/mcp/connect/vscode" description="User settings, HTTP transport." /> " title="Windsurf" href="/docs/mcp/connect/windsurf" description="Codeium MCP config." /> " title="Codex" href="/docs/mcp/connect/codex" description="One terminal command. Covers the CLI, IDE extension, and ChatGPT desktop app." /> " title="Any other MCP client" href="#other-clients" description="Zed, Cline, Continue, and anything else that speaks the protocol." /> ## How the OAuth flow works [#how-the-oauth-flow-works] The hosted server implements the standard MCP authorization spec ([OAuth 2.1](https://oauth.net/2.1/) + PKCE + [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591)). Clients discover the rest from `https://mcp.omnidim.io/.well-known/oauth-authorization-server`. You never paste or copy a client secret. On the first tool call: 1. Your client redirects you to `omnidim.io/oauth/consent` in your browser. 2. You sign in (or sign up) to your OmniDimension account. 3. You see the consent screen with the client name. You approve. 4. The browser hands a one-time code back to the client. 5. The client exchanges the code for an access token and stores it locally. 6. From here, every tool call sends the token as a Bearer header. Tokens are scoped to your OmniDimension account. The server stores upstream credentials encrypted at rest. ## Other clients [#other-clients] Most MCP clients added in 2025 or later support HTTP transport with OAuth. The shape is consistent across them: | Client | Config key | Field | | ------------------------- | ----------------------------- | ----------------------------------- | | Claude Code | (CLI flag) | `--transport http` | | Claude Desktop, Claude.ai | (Settings UI) | custom connector URL | | Cursor | `mcpServers..url` | URL string | | VS Code | `mcp.servers.` | `type: "http"`, `url` | | Windsurf | `mcpServers..serverUrl` | URL string | | Codex | `mcp_servers..url` | URL string (`~/.codex/config.toml`) | | Zed | `context_servers..url` | URL string | | Cline | `mcpServers..url` | URL string | | Continue | `mcpServers.` | `type: streamable-http`, `url` | If your client only supports stdio transport, use the [local server](/docs/mcp/local-server) with an API key instead. ## Revoking access [#revoking-access] You can disconnect at any time: * From inside the client (most clients surface a Disconnect or Remove server option). * From your OmniDimension account at [omnidim.io/api-management](https://omnidim.io/api-management) by removing the corresponding API credential. Either path revokes the access token immediately. Subsequent tool calls return `401`. ## Troubleshooting [#troubleshooting] Double-check the URL ends with `/mcp`. The bare host returns 404. The exact URL is `https://mcp.omnidim.io/mcp`. Make sure your client supports OAuth 2.1 with PKCE and Dynamic Client Registration. Most clients added in 2025 or later do. If yours does not, use the [local stdio server](/docs/mcp/local-server) with an API key instead. Your token may have been revoked. Open [omnidim.io/api-management](https://omnidim.io/api-management) to confirm the credential is present, or remove and re-add the server in your client to start a fresh OAuth flow. The tool catalogue is generated from the public [OpenAPI spec](/openapi.yaml). If an endpoint is in the API reference but not in the MCP tool list, open an issue at [omnidim-mcp-cloud](https://github.com/Omnidim/omnidim-mcp-cloud/issues). # VS Code (/docs/mcp/connect/vscode) > Add OmniDimension to VS Code via user settings. HTTP MCP transport, OAuth sign-in handled in the browser. Open `Cmd/Ctrl + Shift + P` and run **Preferences: Open User Settings (JSON)**. Add: ```json { "mcp": { "servers": { "omnidim": { "type": "http", "url": "https://mcp.omnidim.io/mcp" } } } } ``` Reload the window (`Cmd/Ctrl + Shift + P` → **Developer: Reload Window**). ## Verify [#verify] Open the Copilot Chat or any MCP-capable extension. Ask: ``` What OmniDimension agents do I have? ``` VS Code opens the OmniDimension consent screen in your browser on the first call. Approve and the tool completes. The chat will call `listAgents` and report your agent list. If you see your agents, you're set. ## API key instead of OAuth [#api-key-instead-of-oauth] For headless or CI use, run the [local stdio server](/docs/mcp/local-server) with an API key from [omnidim.io/api-management](https://omnidim.io/api-management) instead. The config block for VS Code is on that page. ## Remove [#remove] Delete the `omnidim` entry from your settings JSON and reload the window. To revoke server-side access, remove the credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Windsurf (/docs/mcp/connect/windsurf) > Add OmniDimension to Windsurf via ~/.codeium/windsurf/mcp_config.json. OAuth sign-in in the browser, no API key handling. Edit `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "omnidim": { "serverUrl": "https://mcp.omnidim.io/mcp" } } } ``` Restart Windsurf. ## Verify [#verify] In the Windsurf chat panel, ask: ``` What OmniDimension agents do I have? ``` Windsurf opens the OmniDimension consent screen in your browser on the first call. Approve and the tool completes. Windsurf will call `listAgents` and report your agent list. If you see your agents, you're set. ## API key instead of OAuth [#api-key-instead-of-oauth] For headless or CI use, run the [local stdio server](/docs/mcp/local-server) with an API key from [omnidim.io/api-management](https://omnidim.io/api-management) instead. The config block for Windsurf is on that page. ## Remove [#remove] Delete the `omnidim` entry from `~/.codeium/windsurf/mcp_config.json` and restart Windsurf. To revoke server-side access, remove the credential at [omnidim.io/api-management](https://omnidim.io/api-management). # Create agent (/docs/api-reference/agents/createAgent) > Create a new agent with the provided configuration. The full config supports transcriber, model, voice, web search, transfer, end-call conditions, post-call actions (email + webhook), ambient background track, initial ringing sound, and multilingual support. **POST** `/agents/create` Create a new agent with the provided configuration. The full config supports transcriber, model, voice, web search, transfer, end-call conditions, post-call actions (email + webhook), ambient background track, initial ringing sound, and multilingual support. > **Voicemail detection is an access-gated feature** that we turn on per account. If it isn't enabled for yours yet, [request access](https://omnidim.io/contact-us?reason=product&lock=1) before configuring the `voicemail` object. ```yaml operationId: createAgent requestBody: required: true content: application/json: schema: allOf: - type: object description: Agent configuration. properties: name: type: string description: Name for the agent. example: Customer Support Agent welcome_message: type: string description: Initial message the agent will say when answering a call. example: Hello! How can I help you today? is_welcome_message_dynamic: type: boolean description: >- When true, the welcome message is treated as a directive the agent uses to generate a tailored greeting for each call, rather than being spoken word for word. When false, the welcome message is spoken exactly as written. is_welcome_message_interruption: type: boolean description: >- Allow the caller to interrupt the welcome message. When false, the agent finishes speaking the welcome before listening. is_interruption_allowed: type: boolean description: >- Global toggle for whether the caller can interrupt the agent mid-sentence at any point in the call. dynamic_variables: type: object description: | Key/value map used to substitute placeholders in the agent's prompt and welcome message at call time. Reference a variable in your prompt with `{{variable_name}}`. Useful for personalising the same agent across many calls. additionalProperties: type: string example: customer_name: Jane Doe order_id: ORD-12345 context_breakdown: type: array description: >- List of context breakdowns, each containing `title`, `body`, and optional `is_enabled`. items: type: object required: - title - body properties: title: type: string description: Title of the breakdown. example: Purpose body: type: string description: Body of the breakdown, the detailed prompt content. example: This agent helps customers with product inquiries and support issues. is_enabled: type: boolean default: true description: Whether this section is included in the prompt. call_type: type: string enum: - Incoming - Outgoing description: Call type of the assistant. timezone: type: string description: >- IANA timezone for this agent, for example `Asia/Kolkata`. Sets the local date and time the agent works with during calls. If not set, the account timezone is used as fallback. Pass an empty string to clear it. example: America/New_York transcriber: type: object description: Configuration for the speech-to-text transcriber. properties: provider: type: string enum: - deepgram_stream - cartesia - sarvam - azure_stream - soniox description: The speech-to-text provider to use. example: deepgram_stream model: type: string enum: - nova-3 - nova-2 description: >- The model to use for transcription (required when provider is `deepgram_stream`). example: nova-3 language: type: string description: | Language code for the transcriber. Format and supported values depend on the provider (e.g. `en-US` for Deepgram, `hi-IN` for Sarvam). Applies regardless of which `provider` is selected. example: en-US silence_timeout_ms: type: integer description: Silence timeout in milliseconds. example: 400 should_apply_noise_reduction: type: boolean description: Reduce background noise on the inbound audio stream before transcription. interruption_min_words: type: integer minimum: 1 description: >- Minimum number of words the caller must say before their speech is treated as an interruption. example: 2 max_call_duration_in_sec: type: integer minimum: 1 description: >- Hard upper bound on call length in seconds. The agent will end the call once this is reached. example: 600 first_ideal_message: type: string description: | First nudge spoken when the caller goes silent past the idle threshold. Set `is_first_ideal_message_dynamic` to `true` to have the LLM regenerate this each time. is_first_ideal_message_dynamic: type: boolean description: >- When true, `first_ideal_message` is treated as a prompt and the LLM generates a fresh nudge each call. second_ideal_message: type: string description: Second nudge spoken if silence continues after the first. is_second_ideal_message_dynamic: type: boolean description: >- When true, `second_ideal_message` is treated as a prompt and the LLM generates a fresh nudge each call. numerals: type: boolean description: Convert numbers from words to digits. punctuate: type: boolean description: Add punctuation to the transcript. smart_format: type: boolean description: Apply smart formatting to the transcript. diarize: type: boolean description: Identify different speakers in the transcript. model: type: object description: Configuration for the language model. properties: model: type: string enum: - azure-gpt-4.1-mini - azure-gpt-4.1-nano - azure-gpt-4o - azure-gpt-4o-mini - gemini-2.5-flash - gemini-2.5-flash-lite - gpt-3.5-turbo - gpt-4.1-mini - gpt-4.1-nano - gpt-4o - gpt-4o-mini - gpt-5.1 - llama-3.3-70b-versatile description: >- The language model to use. The current catalog is returned by the LLM providers list. example: gpt-4.1-mini temperature: type: number minimum: 0 maximum: 1 description: Controls randomness in the model's output (0.0 to 1.0). example: 0.7 voice: type: object description: >- Configuration for the text-to-speech voice. `provider` and `voice_id` identify the voice together, so send both to change it. `provider` on its own is not accepted, and a `voice_id` on its own leaves the voice as it was. The other fields here apply independently. dependentRequired: provider: - voice_id properties: provider: type: string enum: - eleven_labs - google - cartesia - sarvam description: >- The voice provider to use. The current catalog is returned by the TTS providers list. Send `voice_id` alongside it. example: eleven_labs voice_id: type: string description: >- The provider's voice identifier, returned in the `name` field of the voices list (not the numeric `id`). Takes effect when `provider` is sent alongside it. example: JBFqnCBsd6RMkjVDRZzb model: type: string description: | TTS model identifier. Only consumed when `provider` is `cartesia` (e.g. `sonic-3.5`). For ElevenLabs and other providers the model is implied by `voice_id` and this field is ignored. example: sonic-3.5 speech_speed: type: number minimum: 0.5 maximum: 2 default: 1 description: Playback speed multiplier for the agent's voice. 1.0 is normal speed. web_search: type: object description: Configuration for web search capabilities. properties: enabled: type: boolean description: Enable or disable web search functionality. provider: type: string enum: - DuckDuckGo description: The search provider to use. example: DuckDuckGo post_call_actions: type: object description: Side effects that fire once the call ends. Configure email, webhook, or both. properties: email: type: object properties: enabled: type: boolean recipients: type: array description: Email addresses that should receive the notification. items: type: string format: email example: - support@example.com include: type: array description: Which sections to include in the email body. items: type: string enum: - summary - extracted_variables - fullConversation - sentiment extracted_variables: type: array description: Variables the model should pull out of the conversation for the email. items: type: object required: - key - prompt properties: key: type: string description: Unique identifier for the variable in the post-call payload. example: customer_issue prompt: type: string description: Instruction for the model on what to pull out of the conversation. example: Identify the main issue the customer is experiencing. trigger_call_statuses: type: array description: | Call outcomes that should fire this action. Omit to use the default (`completed`, `voicemail_detected`). Pass an explicit list to also include failed calls, no-answers, busy signals, etc. items: type: string enum: - completed - voicemail_detected - failed - no_answer - busy - cancelled example: - completed - voicemail_detected webhook: type: object properties: enabled: type: boolean url: type: string format: uri description: Endpoint that receives a POST with the call payload. example: https://your-webhook-endpoint.com/omnidim-callback include: type: array description: Which sections to include in the webhook body. items: type: string enum: - summary - extracted_variables - fullConversation - sentiment extracted_variables: type: array description: Variables the model should pull out of the conversation for the webhook. items: type: object required: - key - prompt properties: key: type: string description: Unique identifier for the variable in the post-call payload. example: customer_issue prompt: type: string description: Instruction for the model on what to pull out of the conversation. example: Identify the main issue the customer is experiencing. trigger_call_statuses: type: array description: | Call outcomes that should fire this webhook. Omit to use the default (`completed`, `voicemail_detected`). items: type: string enum: - completed - voicemail_detected - failed - no_answer - busy - cancelled example: - completed - failed transfer: type: object description: Conditional call transfer to a human agent or another number. properties: enabled: type: boolean transfer_options: type: array description: >- Where to transfer the call and under what condition. The first matching condition wins. In an agent update, sending this list replaces all saved options. Omit it to keep them unchanged, or send an empty array to clear them. items: type: object required: - number - transfer_condition - transfer_message properties: number: type: string description: >- Primary phone number to transfer to. Include country code with leading `+`. example: '+15551234567' type: type: string enum: - static - dynamic default: static description: | `static` transfers to `number`. `dynamic` lets the agent pick a number at runtime based on the conversation. backup_numbers: type: array description: Fallback numbers tried if the primary is unreachable. items: type: string transfer_condition: type: string description: Natural-language condition that triggers this transfer option. example: Transfer if the customer asks to speak with a human. transfer_message: type: string description: Message the agent says to the caller before executing the transfer. example: Please hold while I connect you to one of our agents. end_call: type: object description: Hang up automatically when a condition is met. properties: enabled: type: boolean condition: type: string description: >- Natural-language condition that triggers ending the call. Only evaluated when `enabled` is true. example: End the call once the customer's issue is resolved. message: type: string description: What the agent says before hanging up. example: Thank you for contacting us. Have a great day! message_type: type: string enum: - static - prompt description: | `static` speaks `message` verbatim. `prompt` treats `message_prompt` as an LLM instruction and generates a fresh closing line each call (useful for matching the caller's language and tone). message_prompt: type: string description: LLM prompt used to generate the closing line when `message_type` is `prompt`. example: End the call politely in the same language the user is speaking. background_track: type: object description: Ambient background noise that plays under the agent's voice. properties: enabled: type: boolean description: Whether to mix the ambient track under the agent's audio. name: type: string enum: - call_center - filler - office - office_1 - restaurant description: Ambient track to mix under the agent. volume: type: number minimum: 0 maximum: 1 default: 0.2 description: Volume level on a 0–1 scale. Default 0.2. tts_volume_reduction: type: number minimum: 0 maximum: 1 description: >- Amount to drop the agent's TTS volume while the ambient track plays, on a 0–1 scale. Helps the voice cut through without raising the overall mix. initial_ringing_sound_enabled: type: boolean description: Plays a ringing tone after the call is picked up, until the agent starts speaking. voicemail: type: object description: >- Voicemail / answering-machine handling for outbound calls. Set this with the nested object shown here; the agent object returns these values as the flat fields `voicemail_enabled` and `voicemail_message`. Voicemail detection is an access-gated feature. If it isn't enabled for your account, [request access](https://omnidim.io/contact-us?reason=product&lock=1). properties: enabled: type: boolean description: Detect voicemail and leave your message instead of speaking to a machine. message: type: string description: Message to leave when voicemail is detected. languages: type: array description: >- Languages the agent should support. Pass each language as a display-name string exactly as it appears in the dashboard's language picker. Unrecognized names are skipped. items: type: string example: - English (India) - Hindi - type: object required: - name - welcome_message - context_breakdown responses: '200': description: Created agent. content: application/json: schema: type: object properties: id: type: integer description: Agent identifier. Use this in subsequent calls to `/agents/{agent_id}`. name: type: string description: Echoes the `name` you submitted. status: type: string description: Build state of the agent. Always `Completed` for newly created agents. example: id: 6365 name: Customer Support Agent status: Completed ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Create a basic agent response = client.agent.create( name="Customer Support Agent", welcome_message="Hello! I'm your customer support assistant. How can I help you today?", context_breakdown=[ {"title": "Purpose", "body": "This agent helps customers with product inquiries and support issues."} ] ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/agents/create" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Save an agent version (/docs/api-reference/agents/createAgentVersion) > Save the agent's current configuration as a named version. **POST** `/agents/{agent_id}/versions` Save the agent's current configuration as a named version. ```yaml operationId: createAgentVersion requestBody: required: true content: application/json: schema: type: object required: - name properties: name: type: string example: v2 pricing script description: Display name for the version. note: type: string example: Shorter opener, new objection handling. description: Optional note describing the version. responses: '200': description: Version saved. content: application/json: schema: type: object properties: success: type: boolean version: type: object description: A saved snapshot of an agent's configuration. properties: id: type: integer version_number: type: integer name: type: string description: >- Display name of the version. `Auto-saved` for automatic versions and `Backup before restore` for system versions. note: type: string kind: type: string enum: - manual - auto - system description: >- `manual`: a person saved it. `auto`: auto-saved after editing went quiet. `system`: a backup taken automatically before a restore. created_by: type: object properties: id: type: integer name: type: string create_date: type: string format: date-time summary: type: object description: At-a-glance counts of what the version contains. properties: llm_service: type: string voice_name: type: string bot_type: type: string languages: type: array items: type: string context_sections: type: integer transfer_options: type: integer post_call_configs: type: integer knowledge_files: type: integer integrations: type: integer flow_nodes: type: integer change_summary: type: object description: >- What changed in this version compared with the previous one. Included when listing versions. `first` is true for the earliest version, which has nothing before it to compare. properties: first: type: boolean count: type: integer description: Number of settings that changed. items: type: array description: The changed settings, most useful first. items: type: object properties: label: type: string example: Transcription old: type: string nullable: true example: Cartesia new: type: string nullable: true example: Soniox example: success: true version: id: 4830 version_number: 6 name: Working pricing script note: Before the new discount flow kind: manual created_by: id: 1234 name: Demo User create_date: '2026-07-28T11:40:00Z' summary: llm_service: gpt-4.1-mini voice_name: asteria bot_type: prompt languages: - English context_sections: 7 transfer_options: 1 post_call_configs: 1 knowledge_files: 2 integrations: 0 flow_nodes: 0 '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent with that ID, or it doesn't belong to you. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Agent not found or access denied '409': description: >- The agent has reached the maximum number of versions that can be created through the API. Delete a version to make room. Versions saved from the dashboard are not limited. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: version_limit_reached error_description: >- This agent has reached the maximum of 50 versions that can be created through the API. Delete a version to make room. ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Save the agent's current configuration as a version agent_id = "your_agent_id_here" response = client.agent.save_version(agent_id, name="Working pricing script", note="Before the new discount flow") print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/agents/{agent_id}/versions" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Delete agent (/docs/api-reference/agents/deleteAgent) > Permanently delete an agent. **DELETE** `/agents/{agent_id}` Permanently delete an agent. ```yaml operationId: deleteAgent responses: '200': description: Agent deleted. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: Agent deleted successfully ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Delete an agent agent_id = "your_agent_id_here" response = client.agent.delete(agent_id) print(response) ``` **curl** ```bash curl -X DELETE "https://omnidim.io/api/v1/agents/{agent_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Delete an agent version (/docs/api-reference/agents/deleteAgentVersion) > Delete a saved version. **DELETE** `/agents/{agent_id}/versions/{version_number}` Delete a saved version. ```yaml operationId: deleteAgentVersion responses: '200': description: Version deleted. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: Version deleted successfully '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent or version matching that ID. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Version not found for this agent ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Delete a saved version agent_id = "your_agent_id_here" version_number = 5 response = client.agent.delete_version(agent_id, version_number) print(response) ``` **curl** ```bash curl -X DELETE "https://omnidim.io/api/v1/agents/{agent_id}/versions/{version_number}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Diff an agent version (/docs/api-reference/agents/diffAgentVersion) > Get a record-level diff for this version. By default it shows what changed in this version compared with the version before it. Use `against=current` to compare with the agent's live config (what restoring this version would change), or `against=` to compare with another version. **GET** `/agents/{agent_id}/versions/{version_number}/diff` Get a record-level diff for this version. By default it shows what changed in this version compared with the version before it. Use `against=current` to compare with the agent's live config (what restoring this version would change), or `against=` to compare with another version. ```yaml operationId: diffAgentVersion parameters: - name: against in: query schema: type: string description: >- What to compare against. Omit or `previous` for the version before this one (the default). `current` for the agent's live config. A version number to compare with that version. responses: '200': description: Diff between the two versions. content: application/json: schema: type: object properties: changed: type: boolean description: Whether there is any difference between the two sides. first: type: boolean description: >- True when this is the earliest version and there is nothing before it to compare (only for the default previous-version comparison). from: type: object nullable: true description: The version being compared from, or null when `first` is true. properties: version_number: type: integer name: type: string to: description: >- The comparison target: an object with the version number and name for a version, or a text label such as `current setup`. oneOf: - type: object properties: version_number: type: integer name: type: string - type: string groups: type: array items: type: object properties: area: type: string description: >- Section of the agent this group of changes belongs to (e.g. Settings, Prompt, Transfer rules, Post-call actions, Knowledge, Integrations, Web widget, Conversation flow, Other settings). changes: type: array items: type: object properties: field: type: string label: type: string old: type: string new: type: string records: type: array items: type: object properties: op: type: string enum: - added - removed - edited label: type: string example: changed: true first: false from: version_number: 4 name: Before pricing tweak to: version_number: 5 name: Working pricing script groups: - area: Settings changes: - field: llm_service label: Model old: gpt-4.1-mini new: gpt-4o-mini - area: Knowledge records: - op: added label: pricing-sheet.pdf '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent or version matching that ID. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Version not found for this agent ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # What changed in this version (vs the previous one) agent_id = "your_agent_id_here" version_number = 5 response = client.agent.diff_version(agent_id, version_number) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/agents/{agent_id}/versions/{version_number}/diff" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Get agent (/docs/api-reference/agents/getAgent) > Get details of a specific agent by ID. The response also includes a `version_history_enabled` boolean showing whether [version history](/docs/api-reference/agents/listAgentVersions) is turned on for the agent's organization. **GET** `/agents/{agent_id}` Get details of a specific agent by ID. The response also includes a `version_history_enabled` boolean showing whether [version history](/docs/api-reference/agents/listAgentVersions) is turned on for the agent's organization. ```yaml operationId: getAgent responses: '200': description: Agent details. content: application/json: schema: type: object description: An AI voice agent. properties: id: type: integer example: 158910 name: type: string example: Customer Support Agent bot_type: type: string example: prompt bot_call_type: type: string enum: - Incoming - Outgoing user_id: type: integer user_name: type: string organization_id: type: integer welcome_message: type: string is_welcome_message_dynamic: type: boolean is_welcome_message_interruption: type: boolean is_interruption_allowed: type: boolean interruption_min_words: type: integer status_of_building_flow: type: string example: Completed context_breakdown: type: array items: type: object properties: id: type: integer context_title: type: string context_body: type: string is_enabled: type: boolean is_auto_generated: type: boolean languages: type: array description: Resolved language records. Each entry is `{value, label}` from `bot.language`. items: type: object properties: value: type: integer label: type: string llm_service: type: string example: gpt-4o-mini llm_temperature: type: number timezone: type: string description: >- IANA timezone set on this agent. `false` when unset, in which case the account timezone applies. example: America/New_York llm_straming_enabled: type: boolean asr_service: type: string example: deepgram_stream asr_deepgram_model: type: string asr_deepgram_numerals: type: boolean asr_deepgram_punctuate: type: boolean asr_deepgram_smart_format: type: boolean asr_deepgram_diarize: type: boolean silence_timeout: type: integer voice_provider: type: string voice_external_id: type: string description: External voice ID from the provider. voice_name: type: string english_voice_accent: type: string eleven_lab_voice_public_owner_id: type: string nullable: true speech_speed: type: number enable_web_search: type: boolean web_search_engine: type: string background_noise_enabled: type: boolean background_noice_name: oneOf: - type: string - type: boolean background_audio_volume: type: number initial_ringing_sound_enabled: type: boolean voicemail_enabled: type: boolean description: >- Whether voicemail detection is on. To change it, send the nested `voicemail.enabled` field when creating or updating the agent. voicemail_message: type: string description: >- Message the agent leaves when voicemail is detected. To change it, send the nested `voicemail.message` field when creating or updating the agent. is_end_call_enabled: type: boolean end_call_condition: type: string end_call_message: type: string end_call_message_type: type: string end_call_message_prompt: type: string max_call_duration_in_sec: type: integer user_idle_threshold_sec: type: integer first_ideal_message: type: string second_ideal_message: type: string last_ideal_message: type: string is_first_ideal_message_dynamic: type: boolean is_second_ideal_message_dynamic: type: boolean is_transfer_enabled: type: boolean is_custom_api_transfer_enabled: type: boolean transfer_options: type: array items: type: object properties: number: type: string type: type: string enum: - static - dynamic backup_numbers: type: array items: type: string transfer_condition: type: string transfer_message: type: string post_call_config_ids: type: array description: Resolved post-call notification configs. One entry per email/webhook destination. items: type: object properties: id: type: integer delivery_method: type: string enum: - Email - Webhook destination: type: string description: Comma-separated recipients for Email; webhook url for Webhook. include_summary: type: boolean include_full_conversation: type: boolean include_sentiment: type: boolean include_extracted_info: type: boolean extracted_variables: type: array items: type: object properties: key: type: string description: type: string attach_file_ids: type: array description: IDs of knowledge-base files attached to this agent. items: type: integer integrations: type: array description: Connected integration records (HubSpot, Salesforce, Slack, etc.). items: type: object flow_data: oneOf: - type: object - type: boolean bot_action_name: oneOf: - type: string - type: boolean version_history_enabled: type: boolean description: Whether version history is turned on for this agent's organization. example: id: 6342 organization_id: 14 name: Outbound Real Estate Lead Qualification Agent context: > # Agent Identity & Purpose # AGENT GLOBAL INSTRUCTIONS ## PERSONA - The agent is a virtual real estate sales assistant. - Represents a real estate company calling a potential property buyer or renter. - Speaks directly to the lead (customer). - Purpose is to confirm interest, qualify the lead, and follow up on previous inquiries. - Overall intent is to be polite, friendly, and professional, with a calm and helpful demeanor. # RESPONSE GENERATION GUIDES - Your responses will be read aloud by a text-to-speech system. - Always use short, simple, conversational sentences. - Never use bullet points, numbered lists, formatted text, or symbols in spoken responses. - End responses with a soft, natural conversational hook when appropriate. - Speak politely and naturally, as if talking to a real person on a phone call. # SCOPE - Can introduce the company and reason for calling. - Can confirm the lead’s interest in buying, selling, or renting property. - Can ask qualifying questions about property needs, budget, and timeline. - Can schedule a follow-up call or appointment with a real estate agent. - Cannot provide legal, financial, or technical advice. - Must politely redirect unsupported queries. # GUARDRAILS - Never pressure, manipulate, or rush the lead. - Never guarantee property availability, pricing, or approval. - Never ask for sensitive information such as social security numbers or full bank details. # Lead Confirmation & Introduction # LEAD CONFIRMATION & INTRODUCTION - Confirm the lead’s name and introduce the company. - State the purpose of the call and reference the lead’s previous inquiry if applicable. Example response: Hello [user_name], this is [agent_name] from [company_name]. I am calling because you showed interest in our real estate listings. Is now a good time to talk? # Interest & Needs Qualification # INTEREST & NEEDS QUALIFICATION - Ask if the lead is still interested in buying, selling, or renting property. - Ask simple qualifying questions about property type, location, budget, and timeline. Example response: Are you still interested in finding a new property? What kind of home or apartment are you looking for? # Follow-Up & Appointment Scheduling # FOLLOW-UP & APPOINTMENT SCHEDULING - Offer to schedule a call or meeting with a licensed real estate agent if the lead is interested. - Confirm preferred times and contact details. Example response: Would you like to schedule a call with one of our real estate agents to discuss your options in more detail? What time works best for you? # Handle Not Interested or Unavailable # HANDLE NOT INTERESTED OR UNAVAILABLE - Respect the lead’s decision if they are not interested or unavailable. - Offer to follow up later if appropriate. - Close politely. Example response: That is completely fine. Thank you for your time. If you change your mind, feel free to contact us anytime. # Closing Statement # CLOSING STATEMENT - End the call politely and thank the lead for their time. Example response: Thank you for speaking with me today. Have a wonderful day. # Agent Knowledge & Context The agent understands real estate lead qualification, polite follow-up etiquette, and how to schedule appointments with licensed agents. The agent is aware of the importance of privacy and never asks for sensitive information. # FAQ Examples User: How did you get my number? Agent: You shared your contact details with us when you showed interest in our real estate listings. User: Can you send me more property details? Agent: I can arrange for a real estate agent to send you more information or schedule a call to discuss your needs. User: Are you a real person? Agent: I am a virtual assistant calling on behalf of [company_name] to help with your real estate inquiry. user_id: 1234 user_name: Demo User bot_action_id: false context_breakdown: - id: 114705 context_title: Agent Identity & Purpose context_body: >- # AGENT GLOBAL INSTRUCTIONS ## PERSONA - The agent is a virtual real estate sales assistant. - Represents a real estate company calling a potential property buyer or renter. - Speaks directly to the lead (customer). - Purpose is to confirm interest, qualify the lead, and follow up on previous inquiries. - Overall intent is to be polite, friendly, and professional, with a calm and helpful demeanor. # RESPONSE GENERATION GUIDES - Your responses will be read aloud by a text-to-speech system. - Always use short, simple, conversational sentences. - Never use bullet points, numbered lists, formatted text, or symbols in spoken responses. - End responses with a soft, natural conversational hook when appropriate. - Speak politely and naturally, as if talking to a real person on a phone call. # SCOPE - Can introduce the company and reason for calling. - Can confirm the lead’s interest in buying, selling, or renting property. - Can ask qualifying questions about property needs, budget, and timeline. - Can schedule a follow-up call or appointment with a real estate agent. - Cannot provide legal, financial, or technical advice. - Must politely redirect unsupported queries. # GUARDRAILS - Never pressure, manipulate, or rush the lead. - Never guarantee property availability, pricing, or approval. - Never ask for sensitive information such as social security numbers or full bank details. is_enabled: true is_auto_generated: false - id: 114706 context_title: Lead Confirmation & Introduction context_body: >- # LEAD CONFIRMATION & INTRODUCTION - Confirm the lead’s name and introduce the company. - State the purpose of the call and reference the lead’s previous inquiry if applicable. Example response: Hello [user_name], this is [agent_name] from [company_name]. I am calling because you showed interest in our real estate listings. Is now a good time to talk? is_enabled: true is_auto_generated: false - id: 114707 context_title: Interest & Needs Qualification context_body: >- # INTEREST & NEEDS QUALIFICATION - Ask if the lead is still interested in buying, selling, or renting property. - Ask simple qualifying questions about property type, location, budget, and timeline. Example response: Are you still interested in finding a new property? What kind of home or apartment are you looking for? is_enabled: true is_auto_generated: false - id: 114708 context_title: Follow-Up & Appointment Scheduling context_body: >- # FOLLOW-UP & APPOINTMENT SCHEDULING - Offer to schedule a call or meeting with a licensed real estate agent if the lead is interested. - Confirm preferred times and contact details. Example response: Would you like to schedule a call with one of our real estate agents to discuss your options in more detail? What time works best for you? is_enabled: true is_auto_generated: false - id: 114709 context_title: Handle Not Interested or Unavailable context_body: >- # HANDLE NOT INTERESTED OR UNAVAILABLE - Respect the lead’s decision if they are not interested or unavailable. - Offer to follow up later if appropriate. - Close politely. Example response: That is completely fine. Thank you for your time. If you change your mind, feel free to contact us anytime. is_enabled: true is_auto_generated: false - id: 114710 context_title: Closing Statement context_body: |- # CLOSING STATEMENT - End the call politely and thank the lead for their time. Example response: Thank you for speaking with me today. Have a wonderful day. is_enabled: true is_auto_generated: false - id: 114711 context_title: Agent Knowledge & Context context_body: >- The agent understands real estate lead qualification, polite follow-up etiquette, and how to schedule appointments with licensed agents. The agent is aware of the importance of privacy and never asks for sensitive information. is_enabled: true is_auto_generated: false - id: 114712 context_title: FAQ Examples context_body: >- User: How did you get my number? Agent: You shared your contact details with us when you showed interest in our real estate listings. User: Can you send me more property details? Agent: I can arrange for a real estate agent to send you more information or schedule a call to discuss your needs. User: Are you a real person? Agent: I am a virtual assistant calling on behalf of [company_name] to help with your real estate inquiry. is_enabled: true is_auto_generated: false llm_service: gpt-4.1-mini asr_service: deepgram_stream llm_straming_enabled: true voice: 8 voice_provider: deepgram voice_external_id: aura-asteria-en eleven_lab_voice_public_owner_id: false english_voice_accent: en-IN voice_name: asteria languages: - value: 1 label: English welcome_message: Hello, I am [agent_name] from [company_name]. Am I speaking with [user_name]? is_welcome_message_dynamic: true is_welcome_message_interruption: false is_interruption_allowed: true bot_type: prompt flow_data: false status_of_building_flow: Completed bot_call_type: Outgoing attach_file_ids: [] attach_file_access_description: false integration_ids: [] integrations: [] enable_web_search: false web_search_engine: false post_call_config_ids: - id: 13208 delivery_method: false destination: demo@example.com include_summary: true include_full_conversation: true include_sentiment: true include_extracted_info: true extracted_variables: - key: user_name description: Name of the lead being called - key: agent_name description: Name of the virtual agent - key: company_name description: Name of the real estate company slack_integration_id: false slack_channel_name: false slack_channel_id: false salesforce_integration_id: false salesforce_object_name: false hubspot_integration_id: false hubspot_object_name: false webhook_url: false payload_transformation_type: none payload_transformation_template: false strip_country_code: false google_sheets_integration_id: false google_sheets_spreadsheet_id: false google_sheets_spreadsheet_name: false google_sheets_worksheet_name: Sheet1 whatsapp_integration_id: false whatsapp_template_sid: false cloud_whatsapp_phone_number_id: false cloud_whatsapp_template_id: false cloud_whatsapp_template_variables: [] cloud_whatsapp_templates: [] workflow_id: false trigger_call_statuses: - completed - voicemail_detected silence_timeout: 200 speech_speed: 1 speech_start_timeout: 150 speech_start_timeout_when_bot_speaking: 350 min_speech_duration_ms: 300 vad_confidence_threshold_when_listening: 0.6 vad_confidence_threshold_when_bot_speaking: 0.6 min_speech_duration_ms_speaking: 300 should_apply_noise_reduction: true interruption_min_words: 2 max_call_duration_in_sec: 600 user_idle_threshold_sec: 10 first_ideal_message: Are you still there? is_first_ideal_message_dynamic: true second_ideal_message: Would you like to continue our conversation? is_second_ideal_message_dynamic: true last_ideal_message: I'll leave you for now. Have a nice day! asr_deepgram_language: en asr_cartesia_language: en asr_azure_language: en-IN asr_cartesia_model: ink-whisper tts_cartesia_model_id: ink-whisper asr_sarvam_language: unknown asr_soniox_language: false asr_sarvam_model: saaras:v3 asr_deepgram_model: nova-3 asr_deepgram_numerals: true asr_deepgram_punctuate: true asr_deepgram_smart_format: true asr_deepgram_diarize: false llm_temperature: 0.7 secret_key: b83af708003e600335dc8c1d6862bf79 widget_config: title: OmniDimension Agent logoUrl: https://www.omnidim.io/logo.png position: bottom-right iframeUrl: http://localhost:3000/chat-widget?secret=b83af708003e600335dc8c1d6862bf79 textColor: '#b1fbf8' background: '#142744' widgetType: chat iframeWidth: 350px iframeHeight: 550px voiceWidgetStyle: full dynamic_variables: [] is_transfer_enabled: false is_custom_api_transfer_enabled: false transfer_options: [] is_end_call_enabled: true end_call_condition: >- End the call when the user says goodbye, thank you, or indicates they are done with the conversation end_call_message: Thank you for calling. Have a great day! Goodbye. end_call_message_type: prompt end_call_message_prompt: End the call politely in the same language user is speaking voicemail_enabled: false voicemail_message: false call_cost_per_min: 0.09 background_noice_name: false background_audio_volume: 0.2 background_noise_enabled: false initial_ringing_sound_enabled: false ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get details of a specific agent agent_id = "your_agent_id_here" response = client.agent.get(agent_id) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/agents/{agent_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List agent versions (/docs/api-reference/agents/listAgentVersions) > List an agent's saved versions, newest first. Includes manual (named) versions, automatic versions, and system backups taken before a restore. **GET** `/agents/{agent_id}/versions` List an agent's saved versions, newest first. Includes manual (named) versions, automatic versions, and system backups taken before a restore. ```yaml operationId: listAgentVersions parameters: - name: pageno in: query schema: type: integer default: 1 description: Page number for pagination. - name: pagesize in: query schema: type: integer default: 30 maximum: 150 description: Number of items per page (max 150). - name: search in: query schema: type: string description: Filter versions whose name matches this substring (case-insensitive). - name: kind in: query schema: type: string enum: - manual - auto - system description: Filter versions by kind. responses: '200': description: Paginated list of versions. content: application/json: schema: type: object properties: versions: type: array items: type: object description: A saved snapshot of an agent's configuration. properties: id: type: integer version_number: type: integer name: type: string description: >- Display name of the version. `Auto-saved` for automatic versions and `Backup before restore` for system versions. note: type: string kind: type: string enum: - manual - auto - system description: >- `manual`: a person saved it. `auto`: auto-saved after editing went quiet. `system`: a backup taken automatically before a restore. created_by: type: object properties: id: type: integer name: type: string create_date: type: string format: date-time summary: type: object description: At-a-glance counts of what the version contains. properties: llm_service: type: string voice_name: type: string bot_type: type: string languages: type: array items: type: string context_sections: type: integer transfer_options: type: integer post_call_configs: type: integer knowledge_files: type: integer integrations: type: integer flow_nodes: type: integer change_summary: type: object description: >- What changed in this version compared with the previous one. Included when listing versions. `first` is true for the earliest version, which has nothing before it to compare. properties: first: type: boolean count: type: integer description: Number of settings that changed. items: type: array description: The changed settings, most useful first. items: type: object properties: label: type: string example: Transcription old: type: string nullable: true example: Cartesia new: type: string nullable: true example: Soniox total_count: type: integer description: Total number of versions matching the current filters. counts: type: object description: Total counts per kind, unaffected by the current filters. properties: all: type: integer manual: type: integer auto: type: integer system: type: integer page: type: integer page_size: type: integer version_history_enabled: type: boolean description: Whether version history is turned on for this agent's organization. example: versions: - id: 4821 version_number: 5 name: Working pricing script note: Before the new discount flow kind: manual created_by: id: 1234 name: Demo User create_date: '2026-07-20T10:15:00Z' summary: llm_service: gpt-4.1-mini voice_name: asteria bot_type: prompt languages: - English context_sections: 7 transfer_options: 1 post_call_configs: 1 knowledge_files: 2 integrations: 0 flow_nodes: 0 - id: 4809 version_number: 4 name: Auto-saved note: '' kind: auto created_by: id: 1234 name: Demo User create_date: '2026-07-18T09:02:00Z' summary: llm_service: gpt-4.1-mini voice_name: asteria bot_type: prompt languages: - English context_sections: 6 transfer_options: 1 post_call_configs: 1 knowledge_files: 2 integrations: 0 flow_nodes: 0 total_count: 5 counts: all: 5 manual: 2 auto: 2 system: 1 page: 1 page_size: 30 version_history_enabled: true '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent with that ID, or it doesn't belong to you. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Agent not found or access denied ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List an agent's saved versions agent_id = "your_agent_id_here" response = client.agent.list_versions(agent_id, pageno=1, pagesize=30) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/agents/{agent_id}/versions" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` ## About version history [#about-version-history] Version history keeps a snapshot of your agent's configuration every time you save one, so you can go back to an earlier setup if a change doesn't work out. The operations in this group list, save, compare, rename, delete, and restore those versions. Every time you save your agent, a version can be kept alongside it: a frozen copy of the configuration at that moment. You can look back at any saved version, compare it with what's running now, or bring it back. There are two kinds. A **named version** is one you saved yourself, usually right before trying something risky. A **backup** is created automatically right before a restore, so restoring never throws away your current setup. Versions you create through the API are capped at 50 per agent. Past that, a save returns `version_limit_reached` with status 409. Delete a version to make room. Saving from the dashboard is not capped. No. Listing, saving, comparing, restoring, renaming and deleting versions work on every plan. Publishing a version, which keeps live traffic on it while you keep editing, is done from the dashboard and needs Early deployers or above. Yes. Restoring saves your current setup as a backup version first, so you can always undo a restore by restoring that backup. Restoring brings back the agent's configuration. If the version references a knowledge file or integration that was deleted since it was saved, that reference can't be reattached; the response reports it so you know what to reconnect by hand. # List agents (/docs/api-reference/agents/listAgents) > Retrieve all agents for the authenticated user with pagination support. **GET** `/agents` Retrieve all agents for the authenticated user with pagination support. ```yaml operationId: listAgents parameters: - name: pageno in: query schema: type: integer default: 1 description: Page number for pagination. - name: pagesize in: query schema: type: integer default: 30 maximum: 150 description: Number of items per page (max 150). - name: name in: query schema: type: string description: Filter agents whose name matches this substring (case-insensitive). responses: '200': description: Paginated list of agents. content: application/json: schema: type: object properties: bots: type: array items: type: object description: An AI voice agent. properties: id: type: integer example: 158910 name: type: string example: Customer Support Agent bot_type: type: string example: prompt bot_call_type: type: string enum: - Incoming - Outgoing user_id: type: integer user_name: type: string organization_id: type: integer welcome_message: type: string is_welcome_message_dynamic: type: boolean is_welcome_message_interruption: type: boolean is_interruption_allowed: type: boolean interruption_min_words: type: integer status_of_building_flow: type: string example: Completed context_breakdown: type: array items: type: object properties: id: type: integer context_title: type: string context_body: type: string is_enabled: type: boolean is_auto_generated: type: boolean languages: type: array description: Resolved language records. Each entry is `{value, label}` from `bot.language`. items: type: object properties: value: type: integer label: type: string llm_service: type: string example: gpt-4o-mini llm_temperature: type: number timezone: type: string description: >- IANA timezone set on this agent. `false` when unset, in which case the account timezone applies. example: America/New_York llm_straming_enabled: type: boolean asr_service: type: string example: deepgram_stream asr_deepgram_model: type: string asr_deepgram_numerals: type: boolean asr_deepgram_punctuate: type: boolean asr_deepgram_smart_format: type: boolean asr_deepgram_diarize: type: boolean silence_timeout: type: integer voice_provider: type: string voice_external_id: type: string description: External voice ID from the provider. voice_name: type: string english_voice_accent: type: string eleven_lab_voice_public_owner_id: type: string nullable: true speech_speed: type: number enable_web_search: type: boolean web_search_engine: type: string background_noise_enabled: type: boolean background_noice_name: oneOf: - type: string - type: boolean background_audio_volume: type: number initial_ringing_sound_enabled: type: boolean voicemail_enabled: type: boolean description: >- Whether voicemail detection is on. To change it, send the nested `voicemail.enabled` field when creating or updating the agent. voicemail_message: type: string description: >- Message the agent leaves when voicemail is detected. To change it, send the nested `voicemail.message` field when creating or updating the agent. is_end_call_enabled: type: boolean end_call_condition: type: string end_call_message: type: string end_call_message_type: type: string end_call_message_prompt: type: string max_call_duration_in_sec: type: integer user_idle_threshold_sec: type: integer first_ideal_message: type: string second_ideal_message: type: string last_ideal_message: type: string is_first_ideal_message_dynamic: type: boolean is_second_ideal_message_dynamic: type: boolean is_transfer_enabled: type: boolean is_custom_api_transfer_enabled: type: boolean transfer_options: type: array items: type: object properties: number: type: string type: type: string enum: - static - dynamic backup_numbers: type: array items: type: string transfer_condition: type: string transfer_message: type: string post_call_config_ids: type: array description: >- Resolved post-call notification configs. One entry per email/webhook destination. items: type: object properties: id: type: integer delivery_method: type: string enum: - Email - Webhook destination: type: string description: Comma-separated recipients for Email; webhook url for Webhook. include_summary: type: boolean include_full_conversation: type: boolean include_sentiment: type: boolean include_extracted_info: type: boolean extracted_variables: type: array items: type: object properties: key: type: string description: type: string attach_file_ids: type: array description: IDs of knowledge-base files attached to this agent. items: type: integer integrations: type: array description: Connected integration records (HubSpot, Salesforce, Slack, etc.). items: type: object flow_data: oneOf: - type: object - type: boolean bot_action_name: oneOf: - type: string - type: boolean version_history_enabled: type: boolean description: Whether version history is turned on for this agent's organization. total_records: type: integer description: Total number of agents on the account. example: bots: - id: 6342 name: Customer Support Agent bot_type: prompt user_name: Demo User user_id: 1234 bot_action_name: false language: - English voice: cgSgspJ2msm6clMCkdW9 voice_provider: deepgram voice_external_id: aura-asteria-en eleven_lab_voice_public_owner_id: false english_voice_accent: en-IN voice_name: asteria llm_service: gpt-4.1-mini llm_straming_enabled: true allow_to_delete: true status_of_building_flow: Completed dynamic_variables: [] flow_data: false bot_call_type: Outgoing attach_file_ids: [] attach_file_access_description: false integration_ids: [] integrations: [] enable_web_search: false web_search_engine: false post_call_config_ids: - id: 13208 delivery_method: false destination: demo@example.com include_summary: true include_full_conversation: true include_sentiment: true include_extracted_info: true extracted_variables: - key: user_name description: Name of the lead being called - key: agent_name description: Name of the virtual agent - key: company_name description: Name of the real estate company slack_integration_id: false slack_channel_name: false slack_channel_id: false salesforce_integration_id: false salesforce_object_name: false hubspot_integration_id: false hubspot_object_name: false webhook_url: false payload_transformation_type: none payload_transformation_template: false strip_country_code: false google_sheets_integration_id: false google_sheets_spreadsheet_id: false google_sheets_spreadsheet_name: false google_sheets_worksheet_name: Sheet1 whatsapp_integration_id: false whatsapp_template_sid: false cloud_whatsapp_phone_number_id: false cloud_whatsapp_template_id: false cloud_whatsapp_template_variables: [] cloud_whatsapp_templates: [] workflow_id: false trigger_call_statuses: - completed - voicemail_detected is_end_call_enabled: true end_call_condition: >- End the call when the user says goodbye, thank you, or indicates they are done with the conversation end_call_message: Thank you for calling. Have a great day! Goodbye. end_call_message_type: prompt end_call_message_prompt: End the call politely in the same language user is speaking voicemail_enabled: false background_noice_name: false background_audio_volume: 0.2 background_noise_enabled: false initial_ringing_sound_enabled: false speech_speed: 1 organization_branch_ids: [] closed_organization_branch_ids: [] total_records: 61 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all agents with pagination response = client.agent.list(page=1, page_size=10) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/agents" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Rename an agent version (/docs/api-reference/agents/renameAgentVersion) > Rename a saved version or edit its note. Version history is immutable otherwise; only the name and note can change. **PATCH** `/agents/{agent_id}/versions/{version_number}` Rename a saved version or edit its note. Version history is immutable otherwise; only the name and note can change. ```yaml operationId: renameAgentVersion requestBody: required: false content: application/json: schema: type: object properties: name: type: string example: v2 pricing script description: New display name for the version. note: type: string example: Shorter opener, new objection handling. description: New note for the version. responses: '200': description: Version renamed. content: application/json: schema: type: object properties: success: type: boolean version: type: object description: A saved snapshot of an agent's configuration. properties: id: type: integer version_number: type: integer name: type: string description: >- Display name of the version. `Auto-saved` for automatic versions and `Backup before restore` for system versions. note: type: string kind: type: string enum: - manual - auto - system description: >- `manual`: a person saved it. `auto`: auto-saved after editing went quiet. `system`: a backup taken automatically before a restore. created_by: type: object properties: id: type: integer name: type: string create_date: type: string format: date-time summary: type: object description: At-a-glance counts of what the version contains. properties: llm_service: type: string voice_name: type: string bot_type: type: string languages: type: array items: type: string context_sections: type: integer transfer_options: type: integer post_call_configs: type: integer knowledge_files: type: integer integrations: type: integer flow_nodes: type: integer change_summary: type: object description: >- What changed in this version compared with the previous one. Included when listing versions. `first` is true for the earliest version, which has nothing before it to compare. properties: first: type: boolean count: type: integer description: Number of settings that changed. items: type: array description: The changed settings, most useful first. items: type: object properties: label: type: string example: Transcription old: type: string nullable: true example: Cartesia new: type: string nullable: true example: Soniox example: success: true version: id: 4821 version_number: 5 name: Pre-launch pricing note: Reviewed with the team kind: manual created_by: id: 1234 name: Demo User create_date: '2026-07-20T10:15:00Z' summary: llm_service: gpt-4.1-mini voice_name: asteria bot_type: prompt languages: - English context_sections: 7 transfer_options: 1 post_call_configs: 1 knowledge_files: 2 integrations: 0 flow_nodes: 0 '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent or version matching that ID. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Version not found for this agent ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Rename a saved version agent_id = "your_agent_id_here" version_number = 5 response = client.agent.rename_version(agent_id, version_number, name="Pre-launch pricing", note="Reviewed with the team") print(response) ``` **curl** ```bash curl -X PATCH "https://omnidim.io/api/v1/agents/{agent_id}/versions/{version_number}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Restore an agent version (/docs/api-reference/agents/restoreAgentVersion) > Restore a version onto the live agent. Your current setup is saved first as a backup version, so restoring is undoable. Configuration is brought back; any knowledge files or integrations that were deleted since this version was saved can't be re-linked, and are reported in `skipped`. **POST** `/agents/{agent_id}/versions/{version_number}/restore` Restore a version onto the live agent. Your current setup is saved first as a backup version, so restoring is undoable. Configuration is brought back; any knowledge files or integrations that were deleted since this version was saved can't be re-linked, and are reported in `skipped`. ```yaml operationId: restoreAgentVersion responses: '200': description: Version restored. content: application/json: schema: type: object properties: success: type: boolean restored_from: type: integer description: The version number that was restored. safety_version: type: integer nullable: true description: >- Version number of the backup taken of the agent's setup right before this restore, or null if no backup was needed. skipped: type: array description: >- References the restore could not bring back (e.g. a knowledge file or integration deleted since this version was saved). items: type: object properties: item: type: string label: type: string reason: type: string example: success: true restored_from: 5 safety_version: 7 skipped: - item: knowledge_file label: old-pricing-sheet.pdf reason: deleted '403': description: Version history is not enabled for this organization. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: feature_disabled error_description: Version history is not enabled for this organization '404': description: No agent or version matching that ID. content: application/json: schema: type: object properties: error: type: string error_description: type: string example: error: not_found error_description: Version not found for this agent ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Restore an agent to a previous version agent_id = "your_agent_id_here" version_number = 5 response = client.agent.restore_version(agent_id, version_number) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/agents/{agent_id}/versions/{version_number}/restore" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Update agent (/docs/api-reference/agents/updateAgent) > Update an existing agent. Send only the fields you want to change. > **Voicemail detection is an access-gated feature** that we turn on per account. If it isn't enabled for yours yet, [request access](https://omnidim.io/contact-us?reason=product&lock=1) before configuring the `voicemail` object below. **PUT** `/agents/{agent_id}` Update an existing agent. Send only the fields you want to change. > **Voicemail detection is an access-gated feature** that we turn on per account. If it isn't enabled for yours yet, [request access](https://omnidim.io/contact-us?reason=product&lock=1) before configuring the `voicemail` object below. ```yaml operationId: updateAgent requestBody: required: true content: application/json: schema: type: object description: Agent configuration. properties: name: type: string description: Name for the agent. example: Customer Support Agent welcome_message: type: string description: Initial message the agent will say when answering a call. example: Hello! How can I help you today? is_welcome_message_dynamic: type: boolean description: >- When true, the welcome message is treated as a directive the agent uses to generate a tailored greeting for each call, rather than being spoken word for word. When false, the welcome message is spoken exactly as written. is_welcome_message_interruption: type: boolean description: >- Allow the caller to interrupt the welcome message. When false, the agent finishes speaking the welcome before listening. is_interruption_allowed: type: boolean description: >- Global toggle for whether the caller can interrupt the agent mid-sentence at any point in the call. dynamic_variables: type: object description: | Key/value map used to substitute placeholders in the agent's prompt and welcome message at call time. Reference a variable in your prompt with `{{variable_name}}`. Useful for personalising the same agent across many calls. additionalProperties: type: string example: customer_name: Jane Doe order_id: ORD-12345 context_breakdown: type: array description: >- List of context breakdowns, each containing `title`, `body`, and optional `is_enabled`. items: type: object required: - title - body properties: title: type: string description: Title of the breakdown. example: Purpose body: type: string description: Body of the breakdown, the detailed prompt content. example: This agent helps customers with product inquiries and support issues. is_enabled: type: boolean default: true description: Whether this section is included in the prompt. call_type: type: string enum: - Incoming - Outgoing description: Call type of the assistant. timezone: type: string description: >- IANA timezone for this agent, for example `Asia/Kolkata`. Sets the local date and time the agent works with during calls. If not set, the account timezone is used as fallback. Pass an empty string to clear it. example: America/New_York transcriber: type: object description: Configuration for the speech-to-text transcriber. properties: provider: type: string enum: - deepgram_stream - cartesia - sarvam - azure_stream - soniox description: The speech-to-text provider to use. example: deepgram_stream model: type: string enum: - nova-3 - nova-2 description: The model to use for transcription (required when provider is `deepgram_stream`). example: nova-3 language: type: string description: | Language code for the transcriber. Format and supported values depend on the provider (e.g. `en-US` for Deepgram, `hi-IN` for Sarvam). Applies regardless of which `provider` is selected. example: en-US silence_timeout_ms: type: integer description: Silence timeout in milliseconds. example: 400 should_apply_noise_reduction: type: boolean description: Reduce background noise on the inbound audio stream before transcription. interruption_min_words: type: integer minimum: 1 description: >- Minimum number of words the caller must say before their speech is treated as an interruption. example: 2 max_call_duration_in_sec: type: integer minimum: 1 description: >- Hard upper bound on call length in seconds. The agent will end the call once this is reached. example: 600 first_ideal_message: type: string description: | First nudge spoken when the caller goes silent past the idle threshold. Set `is_first_ideal_message_dynamic` to `true` to have the LLM regenerate this each time. is_first_ideal_message_dynamic: type: boolean description: >- When true, `first_ideal_message` is treated as a prompt and the LLM generates a fresh nudge each call. second_ideal_message: type: string description: Second nudge spoken if silence continues after the first. is_second_ideal_message_dynamic: type: boolean description: >- When true, `second_ideal_message` is treated as a prompt and the LLM generates a fresh nudge each call. numerals: type: boolean description: Convert numbers from words to digits. punctuate: type: boolean description: Add punctuation to the transcript. smart_format: type: boolean description: Apply smart formatting to the transcript. diarize: type: boolean description: Identify different speakers in the transcript. model: type: object description: Configuration for the language model. properties: model: type: string enum: - azure-gpt-4.1-mini - azure-gpt-4.1-nano - azure-gpt-4o - azure-gpt-4o-mini - gemini-2.5-flash - gemini-2.5-flash-lite - gpt-3.5-turbo - gpt-4.1-mini - gpt-4.1-nano - gpt-4o - gpt-4o-mini - gpt-5.1 - llama-3.3-70b-versatile description: >- The language model to use. The current catalog is returned by the LLM providers list. example: gpt-4.1-mini temperature: type: number minimum: 0 maximum: 1 description: Controls randomness in the model's output (0.0 to 1.0). example: 0.7 voice: type: object description: >- Configuration for the text-to-speech voice. `provider` and `voice_id` identify the voice together, so send both to change it. `provider` on its own is not accepted, and a `voice_id` on its own leaves the voice as it was. The other fields here apply independently. dependentRequired: provider: - voice_id properties: provider: type: string enum: - eleven_labs - google - cartesia - sarvam description: >- The voice provider to use. The current catalog is returned by the TTS providers list. Send `voice_id` alongside it. example: eleven_labs voice_id: type: string description: >- The provider's voice identifier, returned in the `name` field of the voices list (not the numeric `id`). Takes effect when `provider` is sent alongside it. example: JBFqnCBsd6RMkjVDRZzb model: type: string description: | TTS model identifier. Only consumed when `provider` is `cartesia` (e.g. `sonic-3.5`). For ElevenLabs and other providers the model is implied by `voice_id` and this field is ignored. example: sonic-3.5 speech_speed: type: number minimum: 0.5 maximum: 2 default: 1 description: Playback speed multiplier for the agent's voice. 1.0 is normal speed. web_search: type: object description: Configuration for web search capabilities. properties: enabled: type: boolean description: Enable or disable web search functionality. provider: type: string enum: - DuckDuckGo description: The search provider to use. example: DuckDuckGo post_call_actions: type: object description: Side effects that fire once the call ends. Configure email, webhook, or both. properties: email: type: object properties: enabled: type: boolean recipients: type: array description: Email addresses that should receive the notification. items: type: string format: email example: - support@example.com include: type: array description: Which sections to include in the email body. items: type: string enum: - summary - extracted_variables - fullConversation - sentiment extracted_variables: type: array description: Variables the model should pull out of the conversation for the email. items: type: object required: - key - prompt properties: key: type: string description: Unique identifier for the variable in the post-call payload. example: customer_issue prompt: type: string description: Instruction for the model on what to pull out of the conversation. example: Identify the main issue the customer is experiencing. trigger_call_statuses: type: array description: | Call outcomes that should fire this action. Omit to use the default (`completed`, `voicemail_detected`). Pass an explicit list to also include failed calls, no-answers, busy signals, etc. items: type: string enum: - completed - voicemail_detected - failed - no_answer - busy - cancelled example: - completed - voicemail_detected webhook: type: object properties: enabled: type: boolean url: type: string format: uri description: Endpoint that receives a POST with the call payload. example: https://your-webhook-endpoint.com/omnidim-callback include: type: array description: Which sections to include in the webhook body. items: type: string enum: - summary - extracted_variables - fullConversation - sentiment extracted_variables: type: array description: Variables the model should pull out of the conversation for the webhook. items: type: object required: - key - prompt properties: key: type: string description: Unique identifier for the variable in the post-call payload. example: customer_issue prompt: type: string description: Instruction for the model on what to pull out of the conversation. example: Identify the main issue the customer is experiencing. trigger_call_statuses: type: array description: | Call outcomes that should fire this webhook. Omit to use the default (`completed`, `voicemail_detected`). items: type: string enum: - completed - voicemail_detected - failed - no_answer - busy - cancelled example: - completed - failed transfer: type: object description: Conditional call transfer to a human agent or another number. properties: enabled: type: boolean transfer_options: type: array description: >- Where to transfer the call and under what condition. The first matching condition wins. In an agent update, sending this list replaces all saved options. Omit it to keep them unchanged, or send an empty array to clear them. items: type: object required: - number - transfer_condition - transfer_message properties: number: type: string description: Primary phone number to transfer to. Include country code with leading `+`. example: '+15551234567' type: type: string enum: - static - dynamic default: static description: | `static` transfers to `number`. `dynamic` lets the agent pick a number at runtime based on the conversation. backup_numbers: type: array description: Fallback numbers tried if the primary is unreachable. items: type: string transfer_condition: type: string description: Natural-language condition that triggers this transfer option. example: Transfer if the customer asks to speak with a human. transfer_message: type: string description: Message the agent says to the caller before executing the transfer. example: Please hold while I connect you to one of our agents. end_call: type: object description: Hang up automatically when a condition is met. properties: enabled: type: boolean condition: type: string description: >- Natural-language condition that triggers ending the call. Only evaluated when `enabled` is true. example: End the call once the customer's issue is resolved. message: type: string description: What the agent says before hanging up. example: Thank you for contacting us. Have a great day! message_type: type: string enum: - static - prompt description: | `static` speaks `message` verbatim. `prompt` treats `message_prompt` as an LLM instruction and generates a fresh closing line each call (useful for matching the caller's language and tone). message_prompt: type: string description: LLM prompt used to generate the closing line when `message_type` is `prompt`. example: End the call politely in the same language the user is speaking. background_track: type: object description: Ambient background noise that plays under the agent's voice. properties: enabled: type: boolean description: Whether to mix the ambient track under the agent's audio. name: type: string enum: - call_center - filler - office - office_1 - restaurant description: Ambient track to mix under the agent. volume: type: number minimum: 0 maximum: 1 default: 0.2 description: Volume level on a 0–1 scale. Default 0.2. tts_volume_reduction: type: number minimum: 0 maximum: 1 description: >- Amount to drop the agent's TTS volume while the ambient track plays, on a 0–1 scale. Helps the voice cut through without raising the overall mix. initial_ringing_sound_enabled: type: boolean description: Plays a ringing tone after the call is picked up, until the agent starts speaking. voicemail: type: object description: >- Voicemail / answering-machine handling for outbound calls. Set this with the nested object shown here; the agent object returns these values as the flat fields `voicemail_enabled` and `voicemail_message`. Voicemail detection is an access-gated feature. If it isn't enabled for your account, [request access](https://omnidim.io/contact-us?reason=product&lock=1). properties: enabled: type: boolean description: Detect voicemail and leave your message instead of speaking to a machine. message: type: string description: Message to leave when voicemail is detected. languages: type: array description: >- Languages the agent should support. Pass each language as a display-name string exactly as it appears in the dashboard's language picker. Unrecognized names are skipped. items: type: string example: - English (India) - Hindi responses: '200': description: Updated agent. content: application/json: schema: type: object properties: id: type: integer description: Agent identifier. name: type: string status: type: string description: >- Build state of the agent after the update. Always `Completed` once the operation returns. example: id: 6365 name: Updated Support Agent status: Completed ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Update an existing agent agent_id = "your_agent_id_here" update_data = { "name": "Updated Support Agent", "welcome_message": "Hello! I'm your updated support assistant. How can I help you today?", "model": { "temperature": 0.8 } } response = client.agent.update(agent_id, update_data) print(response) ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/agents/{agent_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Add contact to dynamic campaign (/docs/api-reference/bulk-calls/addBulkCallContact) > Push a single contact into a dynamic bulk-call campaign in real time. Dynamic campaigns are created from the dashboard (Bulk Call > Create New Campaign > Dynamic Campaign) and stay alive waiting for contacts, so this webhook is how you feed them from a CRM, form, or automation platform. The contact is queued immediately, and the campaign starts calling it as soon as it is within operating hours. **POST** `/calls/bulk_call/{campaign_id}/add_contact` Push a single contact into a dynamic bulk-call campaign in real time. Dynamic campaigns are created from the dashboard (Bulk Call > Create New Campaign > Dynamic Campaign) and stay alive waiting for contacts, so this webhook is how you feed them from a CRM, form, or automation platform. The contact is queued immediately, and the campaign starts calling it as soon as it is within operating hours. ```yaml operationId: addBulkCallContact parameters: - name: campaign_id in: path required: true description: ID of the dynamic campaign to add the contact to. schema: type: integer requestBody: required: true content: application/json: schema: type: object required: - to_number properties: to_number: type: string description: Contact phone number in international format (e.g., +15551234567). example: '+15551234567' custom_variables: type: object description: | Key-value pairs passed to the agent as context for this call, so the agent can reference them during the conversation (e.g. the contact's name or reason for the call). Match these keys to the variables used in your agent's welcome message or prompt. additionalProperties: true example: name: Demo User interest: Home Insurance metadata: type: object description: | Key-value pairs stored on the contact for your own tracking (e.g. CRM or lead IDs). Not shared with the agent. additionalProperties: true example: crm_lead_id: lead_9876 source: website_form responses: '200': description: | Contact accepted and queued. `campaign_status` reflects the campaign state after the contact was added (`in_progress` when calling resumes, or `waiting` / `auto_paused` when the contact is queued for the next operating window). content: application/json: schema: type: object properties: status: type: string message: type: string campaign_id: type: integer line_id: type: integer nullable: true description: ID of the new contact record in this campaign. campaign_status: type: string to_number: type: string example: status: success message: Contact added successfully campaign_id: 123 line_id: 4567 campaign_status: in_progress to_number: '+15551234567' ``` **cURL** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/123/add_contact" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to_number": "+15551234567", "custom_variables": { "name": "Demo User", "interest": "Home Insurance" }, "metadata": { "crm_lead_id": "lead_9876", "source": "website_form" } }' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/{campaign_id}/add_contact" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Add contacts in bulk (/docs/api-reference/bulk-calls/addBulkCallContacts) > Add up to 1000 contacts to a campaign in one request. **POST** `/calls/bulk_call/{campaign_id}/add_contacts` Add up to 1000 contacts to a campaign in one request. This is the batch form of Add contact to dynamic campaign. Prefer it whenever you have more than a handful: one request of 500 contacts is far cheaper than 500 requests, on your side and ours. Repeated numbers are kept, not merged. If the same number appears twice with different variables, it is called twice, because two rows for one number usually means two real reasons to call. Rows that fail validation are reported in `rejected` and the rest are still added, so a single bad number does not lose the batch. If the campaign has `call_conditions`, rows that do not match are added with status `Skipped`. ```yaml operationId: addBulkCallContacts parameters: - in: path name: campaign_id required: true schema: type: integer description: Id of the bulk call campaign. requestBody: required: true content: application/json: schema: type: object required: - contacts properties: contacts: type: array maxItems: 1000 description: | Each row needs `to_number`. Note this differs from the `contact_list` on Create bulk call, which uses `phone_number` and takes loose keys: here the variables go in an explicit `custom_variables` object. items: type: object required: - to_number properties: to_number: type: string description: Number to call, in international format. example: '+15551234567' custom_variables: type: object description: | Passed to the agent as context for this call, so it can use them in the conversation. additionalProperties: true example: contact_name: Ravi metadata: type: object description: | Stored with the contact and returned on its row in Bulk call results. Not shown to the agent. additionalProperties: true responses: '200': description: | What was added and what was not. `added` and `rejected` together account for every row you sent. content: application/json: schema: type: object properties: status: type: string added: type: array items: type: object properties: line_id: type: integer to_number: type: string rejected: type: array description: | One entry per row that was not added. `index` is the row's position in the array you sent. items: type: object properties: index: type: integer reason: type: string added_count: type: integer rejected_count: type: integer message: type: string campaign_id: type: integer campaign_status: type: string example: status: success message: 1 of 2 contacts added campaign_id: 314 campaign_status: in_progress added: - line_id: 8801 to_number: '+15551234567' added_count: 1 rejected: - index: 1 reason: to_number is not a valid phone number rejected_count: 1 ``` **cURL** ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/add_contacts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ {"to_number": "+15551234567", "custom_variables": {"contact_name": "Ravi"}}, {"to_number": "+15559876543", "custom_variables": {"contact_name": "Priya"}} ] }' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/{campaign_id}/add_contacts" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Add number to rotation pool (/docs/api-reference/bulk-calls/addBulkCallNumber) > Add one of your numbers to the campaign's rotation pool. Works while the campaign is running, which is how you bring in a fresh number when the pool is running out of healthy ones. **POST** `/calls/bulk_call/{bulk_call_id}/numbers` Add one of your numbers to the campaign's rotation pool. Works while the campaign is running, which is how you bring in a fresh number when the pool is running out of healthy ones. The number must belong to you and must not already be in the pool. A number with no agent attached gets this campaign's agent attached automatically; a number attached to a **different** agent is refused. ```yaml operationId: addBulkCallNumber parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. requestBody: required: true content: application/json: schema: type: object required: - phone_number_id properties: phone_number_id: type: integer example: 178 description: One of your numbers, from List phone numbers. responses: '200': description: Number added to the pool. content: application/json: schema: type: object properties: status: type: string message: type: string example: status: success message: Number added to pool ``` **cURL** ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/numbers" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"phone_number_id": 178}' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/numbers" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Bulk call actions (/docs/api-reference/bulk-calls/bulkCallActions) > Pause, resume, or reschedule a running campaign. **PUT** `/calls/bulk_call/{bulk_call_id}` Pause, resume, or reschedule a running campaign. ```yaml operationId: bulkCallActions parameters: - name: bulk_call_id in: path required: true schema: type: integer description: Id of the bulk call campaign. requestBody: required: true content: application/json: schema: type: object required: - action properties: action: type: string example: pause enum: - pause - resume - reschedule description: What to do with the campaign. new_scheduled_datetime: type: string description: New start time for `reschedule`. Format `YYYY-MM-DD HH:MM:SS`. example: '2026-12-25 10:00:00' new_timezone: type: string description: IANA timezone for `reschedule`. example: America/New_York responses: '200': description: | Action applied. `current_status` reflects the new campaign state. `scheduled_datetime` and `timezone` echo back only on `reschedule`. content: application/json: schema: type: object properties: status: type: string message: type: string current_status: type: string scheduled_datetime: type: string timezone: type: string example: status: success message: Bulk call paused successfully current_status: paused ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Pause a bulk call response = client.bulk_call.bulk_call_actions( bulk_call_id=123, action="pause" ) print(response) # Resume a bulk call response = client.bulk_call.bulk_call_actions( bulk_call_id=123, action="resume" ) print(response) # Reschedule a bulk call response = client.bulk_call.bulk_call_actions( bulk_call_id=123, action="reschedule", new_scheduled_datetime="2024-12-26 14:00:00", new_timezone="America/Los_Angeles" ) print(response) ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Cancel bulk call (/docs/api-reference/bulk-calls/cancelBulkCall) > Cancel a bulk-call campaign. **DELETE** `/calls/bulk_call/{bulk_call_id}` Cancel a bulk-call campaign. ```yaml operationId: cancelBulkCall parameters: - name: bulk_call_id in: path required: true schema: type: integer description: Id of the bulk call campaign. responses: '200': description: Campaign cancelled. content: application/json: schema: type: object properties: status: type: string message: type: string current_status: type: string example: status: success message: Bulk call cancelled successfully current_status: cancelled ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Cancel a bulk call response = client.bulk_call.cancel_bulk_call(bulk_call_id=123) print(response) ``` **curl** ```bash curl -X DELETE "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Create bulk call (/docs/api-reference/bulk-calls/createBulkCall) > Create a new bulk-call campaign. Only name, phone_number_id and a contact_list are needed to dial a list now; every other field adds one behaviour on top (drafts, rotation, filtering, scheduling, retries, dynamic feeding). **POST** `/calls/bulk_call/create` Create a new bulk-call campaign. Only name, phone_number_id and a contact_list are needed to dial a list now; every other field adds one behaviour on top (drafts, rotation, filtering, scheduling, retries, dynamic feeding). The guide below the field reference walks the whole journey: the first campaign and its response, each behaviour with a working request, every refusal message with its fix, and the endpoints that operate a campaign once it runs. ```yaml operationId: createBulkCall requestBody: required: true content: application/json: example: name: Customer follow-ups phone_number_id: '177' bot_id: 512 contact_list: - phone_number: '+15551234567' customer_name: John Doe plan: pro - phone_number: '+15559876543' customer_name: Jane Smith plan: trial call_conditions: - column: plan operator: equals value: pro rotation: numbers: - phone_number_id: 177 sequence: 10 - phone_number_id: 178 sequence: 20 strategy: fixed_count calls_per_number: 50 is_scheduled: true scheduled_datetime: '2026-12-25 10:00:00' timezone: America/New_York concurrent_call_limit: 3 retry_config: auto_retry: true auto_retry_schedule: next_day retry_limit: 2 enabled_reschedule_call: true schema: type: object required: - name - phone_number_id properties: name: type: string description: Name of the bulk call campaign. example: Customer Follow-up Campaign phone_number_id: type: string example: '177' description: | The number this campaign calls from. With a `rotation`, the rotation numbers dial instead and this one is the standby. bot_id: type: integer description: | Agent to run the campaign. Defaults to the agent attached to `phone_number_id`; required when the number has none. save_as_draft: type: boolean default: false description: | Store the campaign without dialing; start it later with the start endpoint. See Drafts in the guide below. call_conditions: type: array description: | Dial only the contacts that match every condition; the rest are kept as `Skipped`. See Filtering in the guide below. items: type: object required: - column - operator - value properties: column: type: string description: Key on the contact row to test. example: plan operator: type: string default: equals example: equals enum: - equals - not_equals - contains - greater_than - less_than description: | `contains` is case-insensitive. `greater_than` and `less_than` compare numerically, and a row whose value is not a number fails the condition rather than erroring. value: type: string example: pro rotation: type: object description: | Rotate the campaign across several of your numbers, so no single number burns out. See Rotation in the guide below. required: - numbers properties: numbers: type: array minItems: 1 description: | The numbers to rotate across; each must be yours and listed once. items: type: object required: - phone_number_id properties: phone_number_id: type: integer example: 177 description: One of your numbers, from List phone numbers. sequence: type: integer default: 10 description: Rotation order. Lowest dials first. strategy: type: string default: fixed_count example: fixed_count enum: - fixed_count - cpr_threshold - both - none description: | When to move to the next number: every `calls_per_number` calls, on low health score, both, or never. calls_per_number: type: integer default: 50 example: 50 description: Calls before moving on. Used by `fixed_count` and `both`. health_threshold: type: number default: 30 description: | Health score below which a number is rotated away from. Used by `cpr_threshold` and `both`. fallback: type: string default: pause enum: - pause - continue_best description: | When every number is unhealthy: `pause` the campaign, or `continue_best` with the healthiest one. is_dynamic: type: boolean default: false description: | A dynamic campaign stays alive accepting contacts via the add-contact webhooks, and `contact_list` becomes optional. contact_list: type: array description: | Who to call. Each row needs `phone_number`; any other key reaches the agent as context for that one call. items: type: object required: - phone_number properties: phone_number: type: string description: Phone number in international format (e.g., +15551234567). example: '+15551234567' additionalProperties: true example: - phone_number: '+15551234567' customer_name: John Doe account_id: ACC-12345 - phone_number: '+15559876543' customer_name: Jane Smith account_id: ACC-67890 priority: high is_scheduled: type: boolean default: false description: Whether the campaign should be scheduled for future execution. scheduled_datetime: type: string description: >- Scheduled execution time in format `YYYY-MM-DD HH:MM:SS` (required if `is_scheduled` is true). example: '2026-12-25 10:00:00' timezone: type: string default: UTC description: Timezone for scheduled execution. example: America/New_York concurrent_call_limit: type: integer default: 1 minimum: 1 description: Maximum number of concurrent calls allowed. enabled_reschedule_call: type: boolean default: false description: >- Enable automatic call rescheduling. When enabled the system can reschedule unreachable calls. retry_config: type: object description: Auto-retry configuration object containing retry settings. properties: auto_retry: type: boolean default: false auto_retry_schedule: type: string enum: - immediately - next_day - scheduled_time description: When to retry failed calls. retry_schedule_days: type: integer default: 0 minimum: 0 description: Days to wait before a scheduled retry. retry_schedule_hours: type: integer default: 0 minimum: 0 description: Hours to wait before a scheduled retry. retry_limit: type: integer default: 1 minimum: 1 maximum: 10 description: | Retry attempts, 1 to 10. To disable retries omit it and leave `auto_retry` false; never send `0`. responses: '200': description: | Bulk call campaign created. `current_status` is `scheduled` for `is_scheduled: true`. A dynamic campaign created with no contacts comes back `waiting` (alive, ready for webhook contacts). Otherwise it is dispatching to your concurrency limit immediately after creation. content: application/json: schema: type: object properties: status: type: string message: type: string id: type: integer description: New campaign ID. is_scheduled: type: boolean is_dynamic: type: boolean current_status: type: string example: status: success message: Bulk call created successfully id: 314 is_scheduled: false is_dynamic: false current_status: pending ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) response = client.bulk_call.create_bulk_call({ "name": "Customer Follow-up Campaign", "contact_list": [ { "phone_number": "+15551234567", "customer_name": "John Doe", # optional context key "account_id": "ACC-12345", # optional context key }, { "phone_number": "+15559876543", "customer_name": "Jane Smith", # optional context key "account_id": "ACC-67890", # optional context key }, ], "phone_number_id": "1", # required "is_scheduled": False, # optional "retry_config": { # optional "auto_retry": True, "auto_retry_schedule": "next_day", "retry_limit": 2, }, "enabled_reschedule_call": True, # optional }) print(response) ``` **Dynamic (cURL)** ```bash # Create a dynamic campaign with no contacts up front. # Feed it later with the add_contact webhook. curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/create" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Real-Time Lead Outreach", "phone_number_id": "1", "is_dynamic": true }' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/create" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer follow-ups", "phone_number_id": "177", "bot_id": 512, "contact_list": [ { "phone_number": "+15551234567", "customer_name": "John Doe", "plan": "pro" }, { "phone_number": "+15559876543", "customer_name": "Jane Smith", "plan": "trial" } ], "call_conditions": [ { "column": "plan", "operator": "equals", "value": "pro" } ], "rotation": { "numbers": [ { "phone_number_id": 177, "sequence": 10 }, { "phone_number_id": 178, "sequence": 20 } ], "strategy": "fixed_count", "calls_per_number": 50 }, "is_scheduled": true, "scheduled_datetime": "2026-12-25 10:00:00", "timezone": "America/New_York", "concurrent_call_limit": 3, "retry_config": { "auto_retry": true, "auto_retry_schedule": "next_day", "retry_limit": 2 }, "enabled_reschedule_call": true }' ``` ## Your first campaign [#your-first-campaign] The playground above opens on a request that shows most of what the endpoint accepts. You need far less: this is the smallest request that places calls, and everything else adds one behaviour on top, covered one at a time below. ```json { "name": "Customer follow-ups", "phone_number_id": "177", "contact_list": [ {"phone_number": "+15551234567", "customer_name": "John Doe"}, {"phone_number": "+15559876543", "customer_name": "Jane Smith"} ], "concurrent_call_limit": 3 } ``` Three things to know about this request: * `phone_number_id` is one of your numbers, from [List phone numbers](/docs/api-reference/phone-numbers/listPhoneNumbers). The agent attached to that number runs the calls; pass `bot_id` to use a different agent, or when the number has none attached. * Any extra key on a contact row (`customer_name` here) reaches the agent as context for that one call, so it can greet the person by name or reference their account. * `concurrent_call_limit` is how many calls dial at once, capped by your account's concurrency. The campaign starts dialing the moment this returns: ```json { "status": "success", "message": "Bulk call created successfully and started immediately", "id": 314, "is_scheduled": false, "is_dynamic": false, "current_status": "in_progress", "filtering_stats": { "total_contacts": 2, "filtered_contacts": 2, "skipped_contacts": 0, "filtered_percentage": 100 } } ``` `id` is the handle every other bulk-call endpoint takes. `current_status` tells you what the campaign is doing: `in_progress` here, `draft` if you saved a draft, `scheduled` if you scheduled it, `waiting` for a dynamic campaign with no contacts yet. `filtering_stats` accounts for every contact you sent; with no `call_conditions` it simply confirms all of them are queued. From here, watch progress with [Bulk call live status](/docs/api-reference/bulk-calls/getBulkCallLiveStatus) and read per-contact outcomes with [Bulk call results](/docs/api-reference/bulk-calls/listBulkCallLines). ## Add one behaviour at a time [#add-one-behaviour-at-a-time] | You want to | Add | Covered in | | ---------------------------------------- | ------------------------------ | ------------------------------------------------------------- | | Build the campaign first, start it later | `save_as_draft` | [Drafts](#drafts-build-it-first-start-it-later) | | Spread calls across several numbers | `rotation` | [Rotation](#rotation-spread-calls-across-numbers) | | Call only the contacts that match | `call_conditions` | [Filtering](#filtering-call-only-part-of-your-list) | | Start at a set time, retry no-answers | `is_scheduled`, `retry_config` | [Scheduling and retries](#scheduling-and-retries) | | Feed contacts in real time from a CRM | `is_dynamic` | [Dynamic campaigns](#dynamic-campaigns-feed-contacts-in-live) | ### Drafts: build it first, start it later [#drafts-build-it-first-start-it-later] A campaign created with `save_as_draft: true` is stored without dialing, so you can assemble it across several requests: ```json {"name": "August sweep", "phone_number_id": "177", "save_as_draft": true} ``` Then add contacts in batches of up to 1000 with [Add contacts in bulk](/docs/api-reference/bulk-calls/addBulkCallContacts), set concurrency or calling hours, and fire it with [Start a draft campaign](/docs/api-reference/bulk-calls/startBulkCall) once everything is in place. This is the shape to use when your contact list comes from somewhere else in pieces. ### Rotation: spread calls across numbers [#rotation-spread-calls-across-numbers] One number dialing a whole campaign collects spam reports and stops being answered. A rotation spreads the load and moves off a number before it burns: ```json { "name": "August sweep", "phone_number_id": "177", "rotation": { "numbers": [ {"phone_number_id": 177, "sequence": 1}, {"phone_number_id": 178, "sequence": 2} ], "strategy": "fixed_count", "calls_per_number": 50 } } ``` How the pieces fit: * **The numbers in `rotation.numbers` do the dialing.** `phone_number_id` becomes a standby, used only if every rotation number is paused. It is not added to the rotation for you, so list it in `numbers` too (as here) if you want it taking calls. * **`sequence` is dialing order, lowest first.** Any integers work; `1, 2` is the same as `10, 20`. * **`strategy` defaults to `fixed_count`**: move to the next number every `calls_per_number` calls. `cpr_threshold` instead rotates when a number's health score drops below `health_threshold`, and `both` does whichever comes first. When every number is below the threshold, `fallback` decides: `pause` the campaign, or `continue_best` with the healthiest number. * **Agents follow the campaign.** A rotation number with no agent attached gets this campaign's agent automatically. One attached to a different agent is refused by name, because it would dial while still belonging elsewhere. While it runs, [List rotation pool](/docs/api-reference/bulk-calls/listBulkCallNumbers) shows which number is dialing and how far into its cycle it is, and [Pause or resume a pool number](/docs/api-reference/bulk-calls/setBulkCallNumberActive) takes a number out of rotation without losing its history. ### Filtering: call only part of your list [#filtering-call-only-part-of-your-list] Send a whole export and let the campaign decide who qualifies. Each condition tests one key on your own contact rows, and a contact must pass all of them: ```json { "name": "Renewals", "phone_number_id": "177", "call_conditions": [ {"column": "plan", "operator": "equals", "value": "pro"}, {"column": "balance", "operator": "greater_than", "value": "100"} ], "contact_list": [ {"phone_number": "+15551110001", "plan": "pro", "balance": "240"}, {"phone_number": "+15551110002", "plan": "free", "balance": "900"}, {"phone_number": "+15551110003", "plan": "pro", "balance": "12"} ] } ``` | Contact | plan | balance | Outcome | | ------------ | ---- | ------- | -------------------------- | | +15551110001 | pro | 240 | Called | | +15551110002 | free | 900 | `Skipped`: wrong plan | | +15551110003 | pro | 12 | `Skipped`: balance too low | Contacts that fail are kept with status `Skipped`, not dropped, so [Bulk call results](/docs/api-reference/bulk-calls/listBulkCallLines) still shows them. Check `filtering_stats` in the create response before the campaign gets far: if you meant to call most of the list and `filtered_percentage` comes back tiny, a column name is misspelled. The operators are `equals`, `not_equals`, `contains` (case-insensitive), `greater_than` and `less_than`. `value` is always a string, `"100"` not `100`. A row whose value is not a number fails a numeric comparison rather than erroring, so a stray `"balance": "n/a"` skips that one contact, not the request. One rule tightens later: contacts added to a conditioned campaign through [Add contacts in bulk](/docs/api-reference/bulk-calls/addBulkCallContacts) are rejected, not skipped, when their `custom_variables` are missing a condition's `column`, since a missing column cannot be judged either way. ### Scheduling and retries [#scheduling-and-retries] ```json { "name": "Monday reminders", "phone_number_id": "177", "contact_list": [{"phone_number": "+15551234567"}], "is_scheduled": true, "scheduled_datetime": "2026-09-01 09:00:00", "timezone": "Asia/Kolkata", "retry_config": { "auto_retry": true, "auto_retry_schedule": "next_day", "retry_limit": 2 } } ``` The datetime is interpreted in `timezone` and must be in the future. Retries re-dial contacts that did not connect: `immediately`, `next_day`, or at a `scheduled_time` offset by `retry_schedule_days` and `retry_schedule_hours`. `retry_limit` must be between 1 and 10; to run without retries, leave `auto_retry` false and omit it rather than sending `0`. A campaign that already ran can also be retried on demand with [Retry contacts](/docs/api-reference/bulk-calls/retryBulkCall), no `retry_config` needed up front. ### Dynamic campaigns: feed contacts in live [#dynamic-campaigns-feed-contacts-in-live] Set `is_dynamic: true` and the campaign stays alive accepting contacts in real time instead of completing when its queue drains: ```json {"name": "Inbound leads", "phone_number_id": "177", "is_dynamic": true} ``` `contact_list` is optional here. Feed it from a CRM, form, or automation with [Add contact](/docs/api-reference/bulk-calls/addBulkCallContact) one at a time, or [Add contacts in bulk](/docs/api-reference/bulk-calls/addBulkCallContacts) in batches; each contact is queued on arrival and dialed within the campaign's operating hours. ## When create is refused [#when-create-is-refused] The exact messages the endpoint returns, and what each one means: | The API says | Why | Fix | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | `This phone number has no agent attached. Pass bot_id in the request, or attach an agent to the number first.` | The number cannot answer as anyone | Send `bot_id`, or attach an agent to the number | | `That agent does not exist on your account. Check bot_id.` | `bot_id` is wrong or belongs to another account | Get the id from [List agents](/docs/api-reference/agents/listAgents) | | `... is attached to a different agent.` | Your `bot_id` (or the campaign's agent) contradicts the agent already on that number | Match `bot_id` to the number's agent, re-attach the number, or leave it out | | `... is listed more than once. Each number belongs in the pool once.` | The same `phone_number_id` appears twice in `rotation.numbers` | Send each number once | | `rotation needs a numbers array with at least one of your phone numbers.` | A `rotation` object arrived without `numbers` | Add the numbers, or drop the `rotation` object | | `Phone number ... was not found on your account.` | A rotation number is not yours | Use ids from [List phone numbers](/docs/api-reference/phone-numbers/listPhoneNumbers) | | `Retry limit must be between 1 and 10.` | `retry_limit` was 0 or over 10 | Omit it to disable retries; never send `0` | | `auto_retry_schedule must be one of: immediately, next_day, scheduled_time` | A schedule value outside the enum | Pick one of the three | | `Concurrent call limit must be greater than 0` | `concurrent_call_limit` was 0 or negative | Send at least 1, or omit it | | `scheduled_datetime property is required when is_scheduled is true` | Scheduled with no time | Add `scheduled_datetime` and `timezone` | | `Scheduled datetime must be in the future.` | The time already passed in the given timezone | Check the timezone; times are interpreted in `timezone`, not UTC | | `contact_list is required unless is_dynamic is true` | A static campaign with nobody to call | Send contacts, or make it dynamic | | `Row 3: ...` with `invalid_phone_number` | A contact number failed validation | Numbers are E.164, like `+15551234567`; the row index tells you which | Refusals happen before anything is written, so a refused request never leaves a half-built campaign behind. ## Operating a running campaign [#operating-a-running-campaign] | You want to | Call | | ------------------------------------------- | ------------------------------------------------------------------------------- | | Progress counts for a dashboard | [Bulk call live status](/docs/api-reference/bulk-calls/getBulkCallLiveStatus) | | Per-contact outcomes, variables, recordings | [Bulk call results](/docs/api-reference/bulk-calls/listBulkCallLines) | | Pause, resume, or reschedule the campaign | [Bulk call actions](/docs/api-reference/bulk-calls/bulkCallActions) | | Speed up or slow down | [Change concurrency](/docs/api-reference/bulk-calls/setBulkCallConcurrency) | | See or steer the number rotation | [List rotation pool](/docs/api-reference/bulk-calls/listBulkCallNumbers) | | Restrict dialing to business hours | [Set calling hours](/docs/api-reference/bulk-calls/setBulkCallDailyTimeControl) | | Re-queue contacts that did not connect | [Retry contacts](/docs/api-reference/bulk-calls/retryBulkCall) | ## Runnable code [#runnable-code] [ Runnable code: the whole lifecycle on this page as a Python and TypeScript CLI, printing each request before it sends. Open on GitHub ↗ ](https://github.com/Omnidim/examples/tree/main/bulk-campaigns) # Fetch bulk calls (/docs/api-reference/bulk-calls/fetchBulkCalls) > List bulk-call campaigns with pagination and optional status filter. **GET** `/calls/bulk_call` List bulk-call campaigns with pagination and optional status filter. ```yaml operationId: fetchBulkCalls parameters: - name: pageno in: query description: Page number for pagination. schema: type: integer default: 1 - name: pagesize in: query schema: type: integer default: 10 maximum: 150 description: Items per page (max 150). - name: status in: query schema: type: string description: Filter by status (e.g. completed). responses: '200': description: Paginated list of bulk calls. content: application/json: schema: type: object properties: status: type: string enum: - success - error records: type: array items: type: object description: A bulk-call campaign. properties: id: type: integer example: 11880 name: type: string campaign_type: type: string example: ai_agent user_id: type: integer user_name: type: string bot_id: type: integer bot_name: type: string twilio_number: type: string status: type: string example: completed is_scheduled: type: boolean scheduled_datetime: oneOf: - type: string - type: 'null' recording_file_name: oneOf: - type: string - type: 'null' failed_reason: oneOf: - type: string - type: boolean concurrent_call_limit: type: integer total_calls: type: integer completed_calls: type: integer total_calls_made: type: integer total_calls_to_dispatch: type: integer total_pending_calls: type: integer total_not_reachable_calls: type: integer total_call_transfer_count: type: integer create_date: type: string example: status: success records: - id: 314 name: test campaign_type: ai_agent user_id: 1234 user_name: Demo User twilio_number: '+15551234567' bot_id: 6337 bot_name: Customer Support Agent recording_file_name: null status: completed failed_reason: false is_scheduled: false scheduled_datetime: null create_date: 04/22/2026 22:10:22 concurrent_call_limit: 1 total_calls: 1 completed_calls: 1 total_calls_made: 1 total_calls_to_dispatch: 1 total_pending_calls: 0 total_not_reachable_calls: 1 total_call_transfer_count: 0 total_records: 31 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Fetch all bulk calls with pagination response = client.bulk_call.fetch_bulk_calls(page=1, page_size=10) print(response) # Filter bulk calls by status response = client.bulk_call.fetch_bulk_calls(page=1, page_size=10, status="completed") print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/bulk_call" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Bulk call details (/docs/api-reference/bulk-calls/getBulkCall) > Get detailed information about a bulk-call campaign. **GET** `/calls/bulk_call/{bulk_call_id}` Get detailed information about a bulk-call campaign. ```yaml operationId: getBulkCall parameters: - name: bulk_call_id in: path required: true schema: type: integer description: Id of the bulk call campaign. responses: '200': description: Bulk call details. content: application/json: schema: type: object properties: status: type: string example: success details: type: object description: >- A bulk-call campaign with configuration, execution stats, and the active number pool. properties: id: type: integer description: Campaign ID. name: type: string description: Campaign name. campaign_type: type: string description: Campaign type. Typically `ai_agent` for agent-driven campaigns. user_id: type: integer description: ID of the user who owns the campaign. user_name: type: string description: Display name of the owning user. bot_id: type: integer description: ID of the agent that handles calls for this campaign. bot_name: type: string description: Name of the agent. twilio_number: type: string description: Outbound number used for the campaign (E.164 format). status: type: string description: >- Current campaign status (e.g. `draft`, `scheduled`, `in_progress`, `paused`, `completed`, `cancelled`). is_scheduled: type: boolean description: True when the campaign is scheduled to start at a future time. is_dynamic: type: boolean description: True for dynamic campaigns that source contacts via webhook. scheduled_datetime: oneOf: - type: string - type: 'null' description: >- Scheduled start time, formatted in the campaign timezone. Null when not scheduled. timezone: type: string description: IANA timezone the campaign was scheduled in. recording_file_id: oneOf: - type: integer - type: 'null' description: ID of the uploaded contact list file, if any. recording_file_name: oneOf: - type: string - type: 'null' description: Filename of the uploaded contact list, if any. failed_reason: oneOf: - type: string - type: boolean description: Reason the campaign failed to start, or `false` when there is none. auto_retry: type: boolean description: Whether failed calls are automatically retried. auto_retry_schedule: type: string description: When retries fire (e.g. `immediately`, `after_delay`). retry_schedule_days: type: integer description: Days to wait before retrying when `auto_retry_schedule` uses a delay. retry_schedule_hours: type: integer description: Hours to wait before retrying when `auto_retry_schedule` uses a delay. retry_count: type: integer description: Total retries already performed. retry_limit: type: integer description: Maximum retries allowed per contact. enabled_reschedule_call: type: boolean description: Whether contacts can request a reschedule mid-call. concurrent_call_limit: type: integer description: Maximum number of calls running in parallel. enable_daily_hard_stop: type: boolean description: Whether the campaign pauses each day at `daily_stop_time`. daily_stop_time: type: number description: Daily hard-stop time as a float-of-day (e.g. 18.5 = 18:30). daily_stop_time_formatted: type: string description: Daily hard-stop time formatted as `HH:MM`. daily_stop_timezone: type: string description: IANA timezone for the daily hard-stop. enable_daily_auto_start: type: boolean description: Whether the campaign auto-resumes each day at `daily_start_time`. daily_start_time: type: number description: Daily auto-start time as a float-of-day. daily_start_time_formatted: type: string description: Daily auto-start time formatted as `HH:MM`. daily_start_timezone: type: string description: IANA timezone for the daily auto-start. variable_config: type: array description: Custom variables exposed to the agent for each contact. items: type: object properties: id: type: integer description: Variable ID. variable_name: type: string description: Variable name as referenced in the agent prompt. variable_type: type: string description: Variable data type (e.g. `text`, `number`, `enum`). enum_values: description: Allowed values when `variable_type` is `enum`; `false` otherwise. oneOf: - type: array items: type: string - type: boolean rotation_strategy: type: string description: >- How outbound numbers rotate across the pool (e.g. `none`, `round_robin`, `health_aware`). calls_per_number: type: integer description: Calls placed on a single number before rotating. rotation_health_threshold: type: number description: Health-score threshold below which a number is skipped during rotation. rotation_fallback: type: string description: >- Fallback behavior when no healthy number is available (e.g. `pause`, `continue`). current_number_assignment_id: oneOf: - type: integer - type: 'null' description: ID of the number assignment currently dispatching calls. Null when idle. number_pool_size: type: integer description: Number of phone numbers configured in the rotation pool. number_pool: type: array description: Phone numbers available for outbound rotation. items: type: object properties: id: type: integer description: Pool assignment ID. phone_number_id: type: integer description: Underlying phone-number record ID. phone_number: type: string description: The phone number in E.164 format. phone_number_name: type: string description: Display label for the number. sequence: type: integer description: Position of the number in the rotation order. is_active: type: boolean description: Whether the number is currently eligible for dispatch. calls_dispatched: type: integer description: Calls dispatched on this number in this campaign. calls_picked_up: type: integer description: Calls answered on this number in this campaign. rolling_cpr: oneOf: - type: number - type: 'null' description: Rolling call-pickup rate. Null until enough calls have been dispatched. health_score: oneOf: - type: number - type: 'null' description: Computed health score for the number. Null when not yet computed. total_calls: type: integer description: Total contacts in the campaign. total_calls_to_dispatch: type: integer description: Contacts still eligible for dispatch (excludes skipped). total_calls_made: type: integer description: >- Calls dispatched so far (excludes pending, in-progress, skipped, retry-scheduled). completed_calls: type: integer description: Calls that finished (any non-pending, non-skipped status). pending_calls: type: integer description: Calls in `Pending` or `In Progress` state. failed_calls: type: integer description: Calls that ended with status `Failed`. skipped_calls: type: integer description: Calls explicitly skipped (e.g. duplicates, opt-outs). low_interaction_calls: type: integer description: Calls flagged as `No/Low Interaction`. no_low_interaction_calls: type: integer description: Same as `low_interaction_calls`. Kept for backward compatibility. total_pending_calls: type: integer description: Pending plus in-progress count. total_not_reachable_calls: type: integer description: Calls that ended with `failed`, `no-answer`, or `busy`. total_skipped_calls: type: integer description: Skipped count (matches `skipped_calls`). total_reschedule_calls: type: integer description: Calls that completed with a reschedule request. total_call_transfer_count: type: integer description: Calls where the agent transferred to a human. total_call_cost: type: number description: Total campaign cost in the user's currency. total_voiceai_cost: type: number description: Voice-AI portion of the cost. total_telephony_cost: type: number description: Telephony portion of the cost. total_duration_seconds: type: integer description: Sum of call durations in seconds. avg_duration_seconds: type: integer description: Average call duration across picked-up calls. incoming_calls: type: integer description: Inbound calls received during the campaign window. total_lines: type: integer description: Total contact rows in the campaign list. calls_picked_up: type: integer description: Calls that were answered (completed or completed-with-reschedule). call_status_counts: type: object description: >- Map of per-status counts. Keys are call statuses (e.g. `completed`, `Pending`, `no-answer`, `busy`, `Failed`, `Skipped`); values are integer counts. additionalProperties: type: integer create_date: type: string description: Campaign creation timestamp in the user's timezone. write_date: type: string description: Last update timestamp in the user's timezone. contact_list: type: array description: Original recipient list submitted with the campaign. items: type: object properties: to_number: type: string example: status: success details: id: 314 name: test campaign_type: ai_agent user_id: 1234 user_name: Demo User twilio_number: '+15551234567' bot_id: 6337 bot_name: Customer Support Agent recording_file_id: null recording_file_name: null status: completed total_calls: 1 completed_calls: 1 pending_calls: 0 failed_calls: 0 skipped_calls: 0 low_interaction_calls: 0 create_date: 04/22/2026 22:10:22 write_date: 04/22/2026 22:10:48 is_scheduled: false is_dynamic: false scheduled_datetime: null timezone: Asia/Kolkata failed_reason: false auto_retry: false auto_retry_schedule: immediately retry_schedule_days: 0 retry_schedule_hours: 0 retry_count: 0 retry_limit: 1 enabled_reschedule_call: false concurrent_call_limit: 1 enable_daily_hard_stop: false daily_stop_time: 0 daily_stop_time_formatted: '00:00' daily_stop_timezone: America/Los_Angeles enable_daily_auto_start: false daily_start_time: 0 daily_start_time_formatted: '00:00' daily_start_timezone: America/Los_Angeles variable_config: - id: 45 variable_name: user_name variable_type: text enum_values: false - id: 46 variable_name: agent_name variable_type: text enum_values: false - id: 47 variable_name: company_name variable_type: text enum_values: false rotation_strategy: none calls_per_number: 50 rotation_health_threshold: 30 rotation_fallback: pause current_number_assignment_id: null number_pool_size: 1 number_pool: - id: 200 phone_number_id: 212 phone_number: '+15551234567' phone_number_name: '+15551234567' sequence: 1 is_active: true calls_dispatched: 1 calls_picked_up: 0 rolling_cpr: null health_score: null total_calls_to_dispatch: 1 total_calls_made: 1 calls_picked_up: 0 total_pending_calls: 0 total_not_reachable_calls: 1 no_low_interaction_calls: 0 total_skipped_calls: 0 total_reschedule_calls: 0 total_call_cost: 0 total_voiceai_cost: 0 total_telephony_cost: 0 total_call_transfer_count: 0 call_status_counts: no-answer: 1 total_duration_seconds: 0 avg_duration_seconds: 0 incoming_calls: 0 total_lines: 1 contact_list: - to_number: '+15551234567' ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get detailed information about a bulk call response = client.bulk_call.detail_bulk_call(bulk_call_id=123) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Bulk call live status (/docs/api-reference/bulk-calls/getBulkCallLiveStatus) > Real-time status of a running bulk-call campaign. **GET** `/bulk-call/{bulk_call_id}/live-status` Real-time status of a running bulk-call campaign. ```yaml operationId: getBulkCallLiveStatus parameters: - name: bulk_call_id in: path required: true schema: type: integer description: Id of the bulk call campaign. responses: '200': description: Live status payload. content: application/json: schema: type: object properties: status: type: string bulk_call_id: type: integer campaign_status: type: string summary: type: object properties: total_contacts: type: integer queued: type: integer in_progress: type: integer completed: type: integer failed: type: integer busy: type: integer no_answer: type: integer example: status: success bulk_call_id: 314 campaign_status: completed summary: total_contacts: 1 queued: 0 in_progress: 0 completed: 0 failed: 0 busy: 0 no_answer: 1 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) response = client.bulk_call.get_live_status(bulk_call_id=123) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/bulk-call/{bulk_call_id}/live-status" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Bulk call results (/docs/api-reference/bulk-calls/listBulkCallLines) > Per-contact results for a campaign: what happened on each call, the variables you sent with that contact, and a pointer to the recording. **GET** `/calls/bulk_call/{bulk_call_id}/lines` Per-contact results for a campaign: what happened on each call, the variables you sent with that contact, and a pointer to the recording. ## Paging There is one rule. Call it with no `cursor`, then keep passing back the `next_cursor` you were handed until it comes back `null`. ``` cursor = None while True: page = GET /lines?pagesize=150&cursor={cursor} handle(page["records"]) cursor = page["next_cursor"] if not cursor: break ``` Each call returns a page of rows, oldest first: `pagesize` goes up to 150 and defaults to 30. Cursors are opaque, so pass back the string you were given and never build one. No contact is skipped or returned twice, even while the campaign is still dialing. ## Transcripts are not in the row Each row carries `call.recording_id`, not the conversation. Transcripts reach 212 KB, so carrying them here would make one page tens of megabytes. Fetch the one you want from `GET /calls/logs/{recording_id}`. ```yaml operationId: listBulkCallLines parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. - in: query name: cursor schema: type: string description: | The `next_cursor` from your previous response. Omit it on the first request. Opaque: pass it back unchanged. - in: query name: pagesize schema: type: integer default: 30 maximum: 150 description: Rows per page. Above 150 the request is refused. - in: query name: call_status schema: type: string enum: - Pending - In Progress - completed - voicemail_detected - no-answer - busy - Failed - Skipped - retry_scheduled - cancelled description: Return only contacts in this state. - in: query name: interaction_status schema: type: string description: Return only contacts with this interaction outcome. - in: query name: search schema: type: string description: | An exact phone number, matched against the contact's number and the number that called it. Not a substring search. - in: query name: include_total schema: type: boolean default: false description: | Add `total_records` to the response. It costs a count over the whole filtered campaign, so it is off unless you ask. Ask for it once to fill a header, not on every page of a walk. responses: '200': description: | One page of results. `next_cursor` is `null` on the last page. content: application/json: schema: type: object properties: status: type: string bulk_call_id: type: integer records: type: array items: type: object description: One contact's result inside a bulk-call campaign. properties: id: type: integer description: Id of this contact row in the campaign. to_number: type: string from_number: type: string description: | The number this contact was called from. With a rotation pool this varies between contacts. call_status: type: string interaction_status: type: string failed_reason: type: string nullable: true dispatched_at: type: string nullable: true description: '`YYYY-MM-DD HH:MM:SS`, UTC.' custom_variables: type: object description: The variables submitted with this contact. additionalProperties: true metadata: type: object additionalProperties: true retry_attempt: type: integer retry_scheduled: type: boolean retry_scheduled_datetime: type: string nullable: true reschedule_requested: type: boolean reschedule_datetime: type: string nullable: true reschedule_status: type: string call: type: object nullable: true description: | Null until the contact has actually been called, so a queued or skipped row has no `call`. properties: recording_id: type: integer description: | Fetch the transcript with `GET /calls/logs/{recording_id}`. The transcript is not included here on purpose: they reach 212 KB, which would make a full page tens of megabytes. time_of_call: type: string duration_seconds: type: number duration_minutes: type: number recording_url: type: string call_status: type: string sentiment_score: type: string extracted_variables: type: object description: What the agent extracted during this call. additionalProperties: true is_voicemail: type: boolean answering_machine_detected: type: boolean pagesize: type: integer has_more: type: boolean next_cursor: type: string nullable: true description: Pass back as `cursor`. `null` means you are done. total_records: type: integer description: Only present when `include_total` is true. example: status: success bulk_call_id: 314 records: - id: 156 to_number: '+15551234567' from_number: '+15559876543' call_status: completed interaction_status: '' failed_reason: null dispatched_at: '2026-08-26 14:46:02' custom_variables: contact_name: Ravi Kumar company_name: Acme Corp metadata: {} retry_attempt: 1 retry_scheduled: false retry_scheduled_datetime: null reschedule_requested: false reschedule_datetime: null reschedule_status: pending call: recording_id: 50585 time_of_call: '2026-08-26 14:46:21' duration_seconds: 9 duration_minutes: 0 recording_url: https://backend.omnidim.io/api/v1/recording/50585?token=9d35be29 call_status: completed sentiment_score: '' extracted_variables: {} is_voicemail: false answering_machine_detected: false pagesize: 150 has_more: true next_cursor: MTU2 ``` **cURL** ```bash curl "https://backend.omnidim.io/api/v1/calls/bulk_call/314/lines?pagesize=150" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/lines" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List rotation pool (/docs/api-reference/bulk-calls/listBulkCallNumbers) > The campaign's number pool, and which number is dialing right now. **GET** `/calls/bulk_call/{bulk_call_id}/numbers` The campaign's number pool, and which number is dialing right now. `calls_this_cycle` is what `fixed_count` rotation compares against, so it is the field to watch for the next rotation. `calls_dispatched` is the number's lifetime total across every cycle. ```yaml operationId: listBulkCallNumbers parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. responses: '200': description: The pool and its rotation settings. content: application/json: schema: type: object properties: status: type: string bulk_call_id: type: integer rotation_strategy: type: string calls_per_number: type: integer rotation_health_threshold: type: number rotation_fallback: type: string numbers: type: array items: type: object description: One number in a campaign's rotation pool. properties: assignment_id: type: integer description: | This number's id within the pool. Use it to pause or resume the number. It is not the `phone_number_id`. phone_number_id: type: integer phone_number: type: string sequence: type: integer description: Rotation order. Lowest dials first. is_active: type: boolean description: A paused number stays in the pool and is skipped. is_dialing_now: type: boolean description: Exactly one number in a pool is dialing at a time. calls_dispatched: type: integer description: Lifetime calls from this number in this campaign. calls_this_cycle: type: integer description: | Calls since this number last became active. `fixed_count` rotation compares this against `calls_per_number`, so this is the one to watch for the next rotation. health_score: type: number nullable: true description: Null until enough calls have been placed to score it. example: status: success bulk_call_id: 314 rotation_strategy: fixed_count calls_per_number: 50 rotation_health_threshold: 30 rotation_fallback: pause numbers: - assignment_id: 508 phone_number_id: 74 phone_number: '+15551234567' sequence: 10 is_active: true is_dialing_now: true calls_dispatched: 124 calls_this_cycle: 24 health_score: 82.5 - assignment_id: 509 phone_number_id: 123 phone_number: '+15559876543' sequence: 20 is_active: true is_dialing_now: false calls_dispatched: 100 calls_this_cycle: 50 health_score: null ``` **cURL** ```bash curl "https://backend.omnidim.io/api/v1/calls/bulk_call/314/numbers" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/numbers" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Retry contacts that did not connect (/docs/api-reference/bulk-calls/retryBulkCall) > Re-queue contacts that did not connect, without creating a new campaign. **POST** `/calls/bulk_call/{bulk_call_id}/manual_retry` Re-queue contacts that did not connect, without creating a new campaign. Use it after a campaign finishes with more no-answers than you expected, or when the reason was on your side (a bad window, a number that was having a bad day). Retried contacts keep their original variables. ```yaml operationId: retryBulkCall parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. requestBody: content: application/json: schema: type: object properties: retry_strategy: type: string default: all description: | Which contacts to re-queue. `all` takes everything that did not connect. max_retries: type: integer description: Skip contacts already retried this many times. failure_reasons: type: array description: | Re-queue only contacts that failed for these reasons, for example `no-answer` and `busy`. items: type: string responses: '200': description: How many contacts were re-queued. content: application/json: schema: type: object properties: status: type: string message: type: string current_status: type: string example: status: success message: 42 contacts queued for retry current_status: in_progress ``` **cURL** ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/manual_retry" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"retry_strategy": "all", "max_retries": 2}' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/manual_retry" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Change concurrency (/docs/api-reference/bulk-calls/setBulkCallConcurrency) > Change how many calls the campaign places at once, including while it is running. Raise it to finish sooner, lower it if your team cannot keep up with transfers or your numbers are being answered less. **PUT** `/calls/bulk_call/{bulk_call_id}/concurrency` Change how many calls the campaign places at once, including while it is running. Raise it to finish sooner, lower it if your team cannot keep up with transfers or your numbers are being answered less. The ceiling is your account's concurrency limit. ```yaml operationId: setBulkCallConcurrency parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. requestBody: required: true content: application/json: schema: type: object required: - concurrent_call_limit properties: concurrent_call_limit: type: integer minimum: 1 example: 5 description: Calls to place at once. responses: '200': description: New concurrency. content: application/json: schema: type: object properties: status: type: string message: type: string concurrent_call_limit: type: integer example: status: success message: Concurrency updated concurrent_call_limit: 5 ``` **cURL** ```bash curl -X PUT "https://backend.omnidim.io/api/v1/calls/bulk_call/314/concurrency" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"concurrent_call_limit": 5}' ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/concurrency" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Set calling hours (/docs/api-reference/bulk-calls/setBulkCallDailyTimeControl) > Restrict a campaign to a daily calling window, in the campaign's timezone. Outside the window the campaign holds rather than finishing, and resumes the next day. **PUT** `/calls/bulk_call/{bulk_call_id}/daily-time-control` Restrict a campaign to a daily calling window, in the campaign's timezone. Outside the window the campaign holds rather than finishing, and resumes the next day. ```yaml operationId: setBulkCallDailyTimeControl parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. requestBody: required: true content: application/json: schema: type: object required: - enable_daily_hard_stop - enable_daily_auto_start properties: enable_daily_hard_stop: type: boolean example: true description: Stop dialing at `daily_stop_time` each day. daily_stop_time: type: number example: 18 description: | Hour of day to stop, 0 to 23. Fractions are allowed, so `17.5` is 17:30. Required when the hard stop is on. daily_stop_timezone: type: string example: Asia/Kolkata description: Timezone for the stop time. enable_daily_auto_start: type: boolean example: true description: Resume dialing at `daily_start_time` each day. daily_start_time: type: number example: 9 description: | Hour of day to resume, 0 to 23. Required when auto start is on. daily_start_timezone: type: string example: Asia/Kolkata description: Timezone for the start time. responses: '200': description: Updated calling window. content: application/json: schema: type: object properties: status: type: string message: type: string example: status: success message: Daily time control updated ``` **cURL** ```bash curl -X PUT "https://backend.omnidim.io/api/v1/calls/bulk_call/314/daily-time-control" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enable_daily_auto_start": true, "daily_start_time": 9, "daily_start_timezone": "Asia/Kolkata", "enable_daily_hard_stop": true, "daily_stop_time": 18, "daily_stop_timezone": "Asia/Kolkata" }' ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/daily-time-control" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Pause or resume a pool number (/docs/api-reference/bulk-calls/setBulkCallNumberActive) > Stop or resume dialing from one number in the pool. **PUT** `/calls/bulk_call/{bulk_call_id}/numbers/{assignment_id}` Stop or resume dialing from one number in the pool. Pausing is what you want when a number starts going bad mid-campaign: dialing moves to the next number in sequence and the paused number keeps its history and counters. The last active number of a running campaign cannot be paused, since the campaign would have nothing to dial from. Send the state you want rather than a toggle, so retrying the same request is harmless. `assignment_id` is the number's id **within this campaign's pool**, from List rotation pool. It is not the `phone_number_id`. ```yaml operationId: setBulkCallNumberActive parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. - in: path name: assignment_id required: true schema: type: integer description: The `assignment_id` from List rotation pool. requestBody: required: true content: application/json: schema: type: object required: - is_active properties: is_active: type: boolean example: false description: '`false` pauses the number, `true` resumes it.' responses: '200': description: New state of the number. content: application/json: schema: type: object properties: status: type: string is_active: type: boolean example: status: success is_active: false ``` **cURL** ```bash curl -X PUT "https://backend.omnidim.io/api/v1/calls/bulk_call/314/numbers/508" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"is_active": false}' ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/numbers/{assignment_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Start a draft campaign (/docs/api-reference/bulk-calls/startBulkCall) > Start a campaign that was created with save_as_draft: true. **POST** `/calls/bulk_call/{bulk_call_id}/start` Start a campaign that was created with `save_as_draft: true`. Drafts let you build a campaign over several requests: create it, add contacts in batches, set the number pool, set concurrency, then start when everything is in place. A campaign that is already running, scheduled, or finished cannot be started. ```yaml operationId: startBulkCall parameters: - in: path name: bulk_call_id required: true schema: type: integer description: Id of the bulk call campaign. responses: '200': description: Campaign started. content: application/json: schema: type: object properties: status: type: string message: type: string current_status: type: string example: status: success message: Campaign started current_status: in_progress ``` **cURL** ```bash curl -X POST "https://backend.omnidim.io/api/v1/calls/bulk_call/314/start" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/bulk_call/{bulk_call_id}/start" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Dispatch call (/docs/api-reference/calls/dispatchCall) > Initiate a call to a phone number using a specified agent. The phone number must include a country code with a leading plus. **POST** `/calls/dispatch` Initiate a call to a phone number using a specified agent. The phone number must include a country code with a leading plus. ```yaml operationId: dispatchCall requestBody: required: true content: application/json: schema: type: object required: - agent_id - to_number properties: agent_id: type: integer description: The ID of the agent that will handle the call. example: 158910 to_number: type: string description: The phone number to call. Must include country code (e.g., +15551234567). example: '+15551234567' from_number_id: type: integer description: >- Id of a phone number on your account to place the call from (see the phone number list endpoint). Omit to use the platform's default number. example: 23 call_context: type: object description: >- Optional context information as key-value pairs to be passed to the agent during the call. Can contain any custom fields relevant to your use case. additionalProperties: true example: user_name: Jane Doe account_id: A-2031 last_order: '2026-04-15' metadata: type: object additionalProperties: true description: | Key-value pairs stored on the call for your own tracking (e.g. CRM or lead IDs). Not shared with the agent; echoed back as `metadata` in the post-call webhook so you can correlate results with your records. example: crm_lead_id: lead_9876 source: website_form example: agent_id: 158910 to_number: '+15551234567' from_number_id: 23 call_context: user_name: Jane Doe account_id: A-2031 last_order: '2026-04-15' metadata: crm_lead_id: lead_9876 source: website_form responses: '200': description: Call dispatched. content: application/json: schema: type: object properties: success: type: boolean status: type: string description: >- Dispatch state on the platform side. `dispatched` means the call has been queued for the dialer. example: dispatched requestId: type: integer description: >- Internal call request id. Use it to correlate with `call_request_id` on `/calls/logs`. example: 3166940 custom_variables_count: type: integer description: Number of keys the platform extracted from `call_context`. example: success: true status: dispatched requestId: 3166940 custom_variables_count: 1 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Dispatch a call to a specific number using an agent agent_id = 123 # Replace with your agent ID to_number = "+15551234567" # Must include country code from_number_id = 23 # get the from number id from phone number API. /api/v1/phone_number/list call_context = { "customer_name": "John Doe", "account_id": "ACC-12345", "priority": "high" } response = client.call.dispatch_call(agent_id, to_number, call_context=call_context) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/calls/dispatch" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agent_id": 158910, "to_number": "+15551234567", "from_number_id": 23, "call_context": { "user_name": "Jane Doe", "account_id": "A-2031", "last_order": "2026-04-15" }, "metadata": { "crm_lead_id": "lead_9876", "source": "website_form" } }' ``` # Get call log (/docs/api-reference/calls/getCallLog) > Detailed information about a specific call (duration, status, transcript, sentiment, extracted variables). **GET** `/calls/logs/{call_log_id}` Detailed information about a specific call (duration, status, transcript, sentiment, extracted variables). ```yaml operationId: getCallLog parameters: - name: call_log_id in: path required: true schema: type: integer description: Id of the call log, as returned by the call log list. responses: '200': description: Call log details (singleton in the `call_log_data` array). content: application/json: schema: type: object properties: call_log_data: type: array items: type: object description: A single call log entry. properties: id: type: integer example: 3517893 bot_name: type: string organization_branch_name: type: string is_bot_response: type: boolean time_of_call: type: string description: Display-formatted timestamp (`MM/DD/YYYY HH:MM:SS`). example: 05/06/2026 13:31:54 from_number: type: string to_number: type: string call_direction: type: string enum: - inbound - outbound call_status: type: string example: completed call_duration: type: string description: Display-formatted duration (`M:S`). example: '0:42' call_duration_in_seconds: type: integer call_duration_in_minutes: type: number recording_url: oneOf: - type: string format: uri - type: boolean description: Signed URL to the recording, or `false` if unavailable. internal_recording_url: oneOf: - type: string - type: boolean channel_type: type: string example: Voice sentiment_score: type: string example: Positive sentiment_analysis_details: type: string call_conversation: type: string description: Full transcript with `
`-separated turns. extracted_variables: type: object additionalProperties: true interactions: type: array items: type: object additionalProperties: true post_call_actions: type: object additionalProperties: true cqs_score: type: number prompt_tokens: type: integer completion_tokens: type: integer total_tokens: type: integer aggregated_estimated_cost: type: number model_name: type: string asr_service: type: string tts_service: type: string is_simulation: type: boolean is_voicemail: type: boolean amd_detected: type: boolean hangup_source: oneOf: - type: string - type: 'null' hangup_reason: oneOf: - type: string - type: 'null' call_request_id: type: object additionalProperties: true total_records: type: integer example: call_log_data: - id: 50958 organization_branch_name: '' bot_name: Customer Support Agent is_bot_response: true time_of_call: 05/04/2026 14:46:15 from_number: Chat to_number: Assistant call_direction: outbound call_duration: '0:0' recording_url: false internal_recording_url: false recording_available_at: '' call_conversation: '
user:
LLM: Hello, I am Kevin from OmniDimension support. How may I help you?

user: i awnt ti book a meetig
LLM: Sure, I can help with that. Could you please provide me with your name and email address?

user: aok demo@example.com
LLM: Let''s confirm your email address: demo@example.com Is that correct?

user: y
LLM: Great, you can book a meeting using the link below.
' call_status: completed channel_type: Widget-Chat sentiment_score: Positive sentiment_analysis_details: >- The user requested to book a meeting, providing their email address. The agent confirmed the email and provided a link for booking the meeting successfully. call_type: false is_call_transfer: false status: '' cqs_score: 0 cqs_score_message: false metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 p50_latency: 2.53 p99_latency: 2.79 prompt_tokens: 31727 completion_tokens: 95 total_tokens: 31822 total_tts_speaking_minutes: 0 total_tts_speaking_seconds: 0 llm_prompt: false aggregated_estimated_cost: 0.123 model_name: gpt-4o model_type: chat asr_service: deepgram_stream tts_service: eleven_labs has_issue: false is_simulation: false json_evolution_matrix_score: {} issues: [] call_duration_in_seconds: 0 call_duration_in_minutes: 0 extracted_variables: industry: Not provided use_case: Not provided user_name: Demo User main_purpose: Booking a meeting phone_number: Not provided preferred_language: Not provided monthly_call_volume: Not provided interaction_count_total: 4 voice_insides: false amd_detected: false amd_answered_by: false amd_duration: 0 is_voicemail: false is_fax: false voicemail_type: false hangup_source: null hangup_reason: null user_name: Demo User call_cost: 0 call_request_id: id: false type_of_request: '' interactions: - id: 88247 interaction_sequence: 1 user_query: '' bot_response: Hello, I am Kevin from OmniDimension support. How may I help you? customer_phone_number: false time_of_call: 05/04/2026 14:43:00 llm2_time: 2.349710792 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.349710792 latency_tts: 0 total_response_time: 2.349710792 evaluation_remarks: false total_tokens: 6314 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88248 interaction_sequence: 2 user_query: i awnt ti book a meetig bot_response: >- Sure, I can help with that. Could you please provide me with your name and email address? customer_phone_number: false time_of_call: 05/04/2026 14:43:08 llm2_time: 2.534026668 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.534026668 latency_tts: 0 total_response_time: 2.534026668 evaluation_remarks: false total_tokens: 6318 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88249 interaction_sequence: 3 user_query: aok demo@example.com bot_response: 'Let''s confirm your email address: demo@example.com Is that correct?' customer_phone_number: false time_of_call: 05/04/2026 14:43:13 llm2_time: 1.136648417 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 1.136648417 latency_tts: 0 total_response_time: 1.136648417 evaluation_remarks: false total_tokens: 6351 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88250 interaction_sequence: 4 user_query: 'y' bot_response: Great, you can book a meeting using the link below. customer_phone_number: false time_of_call: 05/04/2026 14:43:18 llm2_time: 2.787461793 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.787461793 latency_tts: 0 total_response_time: 2.787461793 evaluation_remarks: false total_tokens: 12839 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: - args: {} result: >- Calendly card is now visible to the visitor below your reply. Stop and wait for them to tap. Do not ask any follow-up question this turn. success: true time_taken: 0.002235458 function_name: offer_calendly_meeting tts_speaking_duration: 0 post_call_actions: call_recording_webhook_ids: - id: 1870 name: >- Webhook to https://webhook.site/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b on 2026-05-04 14:46:15 webhook_url: https://webhook.site/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b webhook_method: POST payload: >- {"call_id": 50958, "call_sid": "secret_key_fb6cac5ad0ba353d30768b2f6d92dab3_74f67317-cdd6-4b56-9954-440933284998", "bot_id": 6340, "bot_name": "Demo User", "phone_number": "Chat", "call_direction": "outbound", "to_number": "Assistant", "call_request_id": false, "from_number": "Chat", "call_date": "2026-05-04 14:46:15", "start_time": "2026-05-04 14:46:15", "end_time": "2026-05-04 14:46:15", "call_duration": 0, "user_email": "demo@example.com", "call_status": "completed", "hangup_source": false, "recording_url": false, "recording_available_at": null, "low_interaction": true, "call_report": {"summary": "The user requested to book a meeting, providing their email address. The agent confirmed the email and provided a link for booking the meeting successfully.", "sentiment": "Positive", "extracted_variables": {"user_name": "Not provided", "phone_number": "Not provided", "main_purpose": "Booking a meeting", "use_case": "Not provided", "monthly_call_volume": "Not provided", "industry": "Not provided", "preferred_language": "Not provided"}, "full_conversation": " \n user: \n LLM: Hello, I am Kevin from OmniDimension support. How may I help you? \n \n user: i awnt ti book a meetig \n LLM: Sure, I can help with that. Could you please provide me with your name and email address? \n \n user: aok demo@example.com \n LLM: Let's confirm your email address: demo@example.com Is that correct? \n \n user: y \n LLM: Great, you can book a meeting using the link below. \n", "interactions": [{"sequence": 1, "user_query": "", "bot_response": "Hello, I am Kevin from OmniDimension support. How may I help you?", "time": "2026-05-04 14:43:00"}, {"sequence": 2, "user_query": "i awnt ti book a meetig", "bot_response": "Sure, I can help with that. Could you please provide me with your name and email address?", "time": "2026-05-04 14:43:08"}, {"sequence": 3, "user_query": "aok demo@example.com", "bot_response": "Let's confirm your email address: demo@example.com Is that correct?", "time": "2026-05-04 14:43:13"}, {"sequence": 4, "user_query": "y", "bot_response": "Great, you can book a meeting using the link below.", "time": "2026-05-04 14:43:18"}]}} status: sent response_code: 200 response_body: >- This URL has no default content configured. [Change response in Webhook.site](https://webhook.site/#!/edit/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b). error_message: false call_recording_id: 50958 user_id: 1234 create_date: '2026-05-04 14:46:15' create_by: Demo User email_service_ids: [] hubspot_service_ids: [] slack_service_ids: [] salesforce_service_ids: [] google_sheets_service_ids: [] total_records: 1 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get details of a specific call log call_log_id = "your_call_log_id_here" response = client.call.get_call_log(call_log_id) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/logs/{call_log_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List call logs (/docs/api-reference/calls/listCallLogs) > Retrieve call logs with pagination and optional filtering. **GET** `/calls/logs` Retrieve call logs with pagination and optional filtering. ```yaml operationId: listCallLogs parameters: - name: pageno in: query schema: type: integer default: 1 description: Page number for pagination. - name: pagesize in: query schema: type: integer default: 30 maximum: 150 description: Number of items per page. - name: agentid in: query schema: type: integer description: Filter by agent ID. - name: call_status in: query description: Filter by call outcome. schema: type: string enum: - completed - busy - failed - no-answer - name: bulk_call_id in: query schema: type: integer description: Filter by bulk-call campaign ID. responses: '200': description: Paginated list of call logs. content: application/json: schema: type: object properties: call_log_data: type: array items: type: object description: A single call log entry. properties: id: type: integer example: 3517893 bot_name: type: string organization_branch_name: type: string is_bot_response: type: boolean time_of_call: type: string description: Display-formatted timestamp (`MM/DD/YYYY HH:MM:SS`). example: 05/06/2026 13:31:54 from_number: type: string to_number: type: string call_direction: type: string enum: - inbound - outbound call_status: type: string example: completed call_duration: type: string description: Display-formatted duration (`M:S`). example: '0:42' call_duration_in_seconds: type: integer call_duration_in_minutes: type: number recording_url: oneOf: - type: string format: uri - type: boolean description: Signed URL to the recording, or `false` if unavailable. internal_recording_url: oneOf: - type: string - type: boolean channel_type: type: string example: Voice sentiment_score: type: string example: Positive sentiment_analysis_details: type: string call_conversation: type: string description: Full transcript with `
`-separated turns. extracted_variables: type: object additionalProperties: true interactions: type: array items: type: object additionalProperties: true post_call_actions: type: object additionalProperties: true cqs_score: type: number prompt_tokens: type: integer completion_tokens: type: integer total_tokens: type: integer aggregated_estimated_cost: type: number model_name: type: string asr_service: type: string tts_service: type: string is_simulation: type: boolean is_voicemail: type: boolean amd_detected: type: boolean hangup_source: oneOf: - type: string - type: 'null' hangup_reason: oneOf: - type: string - type: 'null' call_request_id: type: object additionalProperties: true total_records: type: integer example: call_log_data: - id: 50958 organization_branch_name: '' bot_name: Customer Support Agent is_bot_response: true time_of_call: 05/04/2026 14:46:15 from_number: Chat to_number: Assistant call_direction: outbound call_duration: '0:0' recording_url: false internal_recording_url: false recording_available_at: '' call_conversation: '
user:
LLM: Hello, I am Kevin from OmniDimension support. How may I help you?

user: i awnt ti book a meetig
LLM: Sure, I can help with that. Could you please provide me with your name and email address?

user: aok demo@example.com
LLM: Let''s confirm your email address: demo@example.com Is that correct?

user: y
LLM: Great, you can book a meeting using the link below.
' call_status: completed channel_type: Widget-Chat sentiment_score: Positive sentiment_analysis_details: >- The user requested to book a meeting, providing their email address. The agent confirmed the email and provided a link for booking the meeting successfully. call_type: false is_call_transfer: false status: '' cqs_score: 0 cqs_score_message: false metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 p50_latency: 2.53 p99_latency: 2.79 prompt_tokens: 31727 completion_tokens: 95 total_tokens: 31822 total_tts_speaking_minutes: 0 total_tts_speaking_seconds: 0 llm_prompt: false aggregated_estimated_cost: 0.123 model_name: gpt-4o model_type: chat asr_service: deepgram_stream tts_service: eleven_labs has_issue: false is_simulation: false json_evolution_matrix_score: {} issues: [] call_duration_in_seconds: 0 call_duration_in_minutes: 0 extracted_variables: industry: Not provided use_case: Not provided user_name: Demo User main_purpose: Booking a meeting phone_number: Not provided preferred_language: Not provided monthly_call_volume: Not provided interaction_count_total: 4 voice_insides: false amd_detected: false amd_answered_by: false amd_duration: 0 is_voicemail: false is_fax: false voicemail_type: false hangup_source: null hangup_reason: null user_name: Demo User call_cost: 0 call_request_id: id: false type_of_request: '' interactions: - id: 88247 interaction_sequence: 1 user_query: '' bot_response: Hello, I am Kevin from OmniDimension support. How may I help you? customer_phone_number: false time_of_call: 05/04/2026 14:43:00 llm2_time: 2.349710792 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.349710792 latency_tts: 0 total_response_time: 2.349710792 evaluation_remarks: false total_tokens: 6314 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88248 interaction_sequence: 2 user_query: i awnt ti book a meetig bot_response: >- Sure, I can help with that. Could you please provide me with your name and email address? customer_phone_number: false time_of_call: 05/04/2026 14:43:08 llm2_time: 2.534026668 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.534026668 latency_tts: 0 total_response_time: 2.534026668 evaluation_remarks: false total_tokens: 6318 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88249 interaction_sequence: 3 user_query: aok demo@example.com bot_response: 'Let''s confirm your email address: demo@example.com Is that correct?' customer_phone_number: false time_of_call: 05/04/2026 14:43:13 llm2_time: 1.136648417 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 1.136648417 latency_tts: 0 total_response_time: 1.136648417 evaluation_remarks: false total_tokens: 6351 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: false tts_speaking_duration: 0 - id: 88250 interaction_sequence: 4 user_query: 'y' bot_response: Great, you can book a meeting using the link below. customer_phone_number: false time_of_call: 05/04/2026 14:43:18 llm2_time: 2.787461793 asr_time: 0 tts_time: 0 metric_score_intent: 0 metric_score_relevance: 0 metric_score_latency: 0 metric_score_coherence: 0 latency_llm: 2.787461793 latency_tts: 0 total_response_time: 2.787461793 evaluation_remarks: false total_tokens: 12839 has_issue: false total_cost: 0 issues: [] json_evolution_matrix_score: {} function_call_data: - args: {} result: >- Calendly card is now visible to the visitor below your reply. Stop and wait for them to tap. Do not ask any follow-up question this turn. success: true time_taken: 0.002235458 function_name: offer_calendly_meeting tts_speaking_duration: 0 post_call_actions: call_recording_webhook_ids: - id: 1870 name: >- Webhook to https://webhook.site/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b on 2026-05-04 14:46:15 webhook_url: https://webhook.site/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b webhook_method: POST payload: >- {"call_id": 50958, "call_sid": "secret_key_fb6cac5ad0ba353d30768b2f6d92dab3_74f67317-cdd6-4b56-9954-440933284998", "bot_id": 6340, "bot_name": "Demo User", "phone_number": "Chat", "call_direction": "outbound", "to_number": "Assistant", "call_request_id": false, "from_number": "Chat", "call_date": "2026-05-04 14:46:15", "start_time": "2026-05-04 14:46:15", "end_time": "2026-05-04 14:46:15", "call_duration": 0, "user_email": "demo@example.com", "call_status": "completed", "hangup_source": false, "recording_url": false, "recording_available_at": null, "low_interaction": true, "call_report": {"summary": "The user requested to book a meeting, providing their email address. The agent confirmed the email and provided a link for booking the meeting successfully.", "sentiment": "Positive", "extracted_variables": {"user_name": "Not provided", "phone_number": "Not provided", "main_purpose": "Booking a meeting", "use_case": "Not provided", "monthly_call_volume": "Not provided", "industry": "Not provided", "preferred_language": "Not provided"}, "full_conversation": " \n user: \n LLM: Hello, I am Kevin from OmniDimension support. How may I help you? \n \n user: i awnt ti book a meetig \n LLM: Sure, I can help with that. Could you please provide me with your name and email address? \n \n user: aok demo@example.com \n LLM: Let's confirm your email address: demo@example.com Is that correct? \n \n user: y \n LLM: Great, you can book a meeting using the link below. \n", "interactions": [{"sequence": 1, "user_query": "", "bot_response": "Hello, I am Kevin from OmniDimension support. How may I help you?", "time": "2026-05-04 14:43:00"}, {"sequence": 2, "user_query": "i awnt ti book a meetig", "bot_response": "Sure, I can help with that. Could you please provide me with your name and email address?", "time": "2026-05-04 14:43:08"}, {"sequence": 3, "user_query": "aok demo@example.com", "bot_response": "Let's confirm your email address: demo@example.com Is that correct?", "time": "2026-05-04 14:43:13"}, {"sequence": 4, "user_query": "y", "bot_response": "Great, you can book a meeting using the link below.", "time": "2026-05-04 14:43:18"}]}} status: sent response_code: 200 response_body: >- This URL has no default content configured. [Change response in Webhook.site](https://webhook.site/#!/edit/94b3db04-2a3f-480f-86f5-6bcc84b4cc5b). error_message: false call_recording_id: 50958 user_id: 1234 create_date: '2026-05-04 14:46:15' create_by: Demo User email_service_ids: [] hubspot_service_ids: [] slack_service_ids: [] salesforce_service_ids: [] google_sheets_service_ids: [] total_records: 1672 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get all call logs with pagination response = client.call.get_call_logs(page=1, page_size=10) print(response) # Filter by agent ID response = client.call.get_call_logs(page=1, page_size=10, agent_id=123) print(response) # Filter by call status response = client.call.get_call_logs(page=1, call_status="completed") print(response) # Filter by bulk call campaign response = client.call.get_call_logs(page=1, page_size=50, bulk_call_id=123) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/calls/logs" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Attach files to agent (/docs/api-reference/knowledge-base/attachKnowledgeBaseFiles) > Attach multiple knowledge-base files to an agent. **POST** `/knowledge_base/attach` Attach multiple knowledge-base files to an agent. ```yaml operationId: attachKnowledgeBaseFiles requestBody: required: true content: application/json: schema: type: object required: - file_ids - agent_id properties: file_ids: type: array minItems: 1 items: type: integer description: List of knowledge-base file IDs to attach. example: - 17686 agent_id: type: integer description: ID of the agent to attach files to. example: 158910 when_to_use: type: string description: Instruction to the agent on when to consult these files. example: Use these documents to answer billing-related questions. responses: '200': description: Files attached. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: 1 files attached to agent successfully ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Attach files to an agent file_ids = [123, 456] # Replace with your file IDs agent_id = 789 # Replace with your agent ID response = client.knowledge_base.attach( file_ids, agent_id, when_to_use="Use KB When User ask for pricing" ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/knowledge_base/attach" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Check file upload capability (/docs/api-reference/knowledge-base/canUploadFile) > Check whether a file can be uploaded based on size and type. **POST** `/knowledge_base/can_upload` Check whether a file can be uploaded based on size and type. ```yaml operationId: canUploadFile requestBody: required: true content: application/json: schema: type: object required: - file_size - file_type properties: file_size: type: integer minimum: 1 description: Size in bytes. example: 524288 file_type: type: string description: File extension. Only `pdf` is accepted. example: pdf responses: '200': description: Upload capability and quota information. content: application/json: schema: type: object properties: success: type: boolean message: type: string quota: type: object description: Knowledge-base storage quota in megabytes. properties: total: type: number used: type: number remaining: type: number example: success: true message: File can be uploaded quota: total: 10 used: 0 remaining: 10 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Check if a file of the given size can be uploaded file_size = 1024 * 1024 # 1MB file response = client.knowledge_base.can_upload(file_size) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/knowledge_base/can_upload" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Delete file from knowledge base (/docs/api-reference/knowledge-base/deleteKnowledgeBaseFile) > Permanently delete a file. Removes it from any attached agents. Cannot be undone. **POST** `/knowledge_base/delete` Permanently delete a file. Removes it from any attached agents. Cannot be undone. ```yaml operationId: deleteKnowledgeBaseFile requestBody: required: true content: application/json: schema: type: object required: - file_id properties: file_id: type: integer description: ID of the file to delete. example: 17686 responses: '200': description: File deleted. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: File deleted successfully ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Delete a file from the knowledge base file_id = 123 # Replace with your file ID response = client.knowledge_base.delete(file_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/knowledge_base/delete" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Detach files from agent (/docs/api-reference/knowledge-base/detachKnowledgeBaseFiles) > Detach multiple knowledge-base files from an agent. **POST** `/knowledge_base/detach` Detach multiple knowledge-base files from an agent. ```yaml operationId: detachKnowledgeBaseFiles requestBody: required: true content: application/json: schema: type: object required: - file_ids - agent_id properties: file_ids: type: array minItems: 1 items: type: integer description: List of knowledge-base file IDs to detach. example: - 17686 agent_id: type: integer description: ID of the agent to detach files from. example: 158910 responses: '200': description: Files detached. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: 1 files detached from agent successfully ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Detach files from an agent file_ids = [123, 456] # Replace with your file IDs agent_id = 789 # Replace with your agent ID response = client.knowledge_base.detach(file_ids, agent_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/knowledge_base/detach" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List knowledge base files (/docs/api-reference/knowledge-base/listKnowledgeBaseFiles) > List all knowledge-base files for the authenticated user. **GET** `/knowledge_base/list` List all knowledge-base files for the authenticated user. ```yaml operationId: listKnowledgeBaseFiles responses: '200': description: List of files. content: application/json: schema: type: object properties: success: type: boolean files: type: array items: type: object description: A file uploaded to the knowledge base. properties: id: type: integer example: 17686 name: type: string example: customer_call.mp3 original_filename: type: string example: customer_call.mp3 file_size: type: integer description: Size in bytes. mime_type: type: string example: audio/mpeg download_url: type: string format: uri upload_status: type: string example: uploaded upload_date: type: string example: 04/11/2026 16:24:00 user_name: type: string user_id: type: integer organization_id: type: integer example: success: true files: - id: 964 name: customer_call.mp3 original_filename: customer_call.mp3 file_size: 1145133 mime_type: audio/mpeg download_url: >- https://omnidim.s3.amazonaws.com/live_uploads/1234/20260411_162353_customer_call.mp3?AWSAccessKeyId=AKIA...&Signature=...&Expires=... upload_status: uploaded upload_date: 04/11/2026 16:24:00 user_name: Demo User user_id: 1234 organization_id: 14 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all knowledge base files response = client.knowledge_base.list() print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/knowledge_base/list" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Upload file to knowledge base (/docs/api-reference/knowledge-base/uploadKnowledgeBaseFile) > Upload a PDF file. The file content must be Base64 encoded. **POST** `/knowledge_base/create` Upload a PDF file. The file content must be Base64 encoded. ```yaml operationId: uploadKnowledgeBaseFile requestBody: required: true content: application/json: schema: type: object required: - file - filename properties: file: type: string description: Base64-encoded file content. filename: type: string description: Filename including the `.pdf` extension. example: sample.pdf responses: '200': description: Uploaded file metadata. content: application/json: schema: type: object properties: success: type: boolean message: type: string file: type: object description: A file uploaded to the knowledge base. properties: id: type: integer example: 17686 name: type: string example: customer_call.mp3 original_filename: type: string example: customer_call.mp3 file_size: type: integer description: Size in bytes. mime_type: type: string example: audio/mpeg download_url: type: string format: uri upload_status: type: string example: uploaded upload_date: type: string example: 04/11/2026 16:24:00 user_name: type: string user_id: type: integer organization_id: type: integer example: success: true message: File uploaded successfully file: id: 964 name: sample.pdf original_filename: sample.pdf file_size: 524288 mime_type: application/pdf upload_status: uploaded upload_date: 05/08/2026 10:30:00 user_id: 1234 user_name: Demo User organization_id: 14 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Upload a file to the knowledge base import base64 file_path = "sample.pdf" # Path to your PDF file file_name = "sample.pdf" # Name for the file in the knowledge base with open(file_path, "rb") as file: file_data = base64.b64encode(file.read()).decode('utf-8') response = client.knowledge_base.create(file_data, file_name) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/knowledge_base/create" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Attach phone number to agent (/docs/api-reference/phone-numbers/attachPhoneNumber) > Attach an account-owned phone number to an existing agent. **POST** `/phone_number/attach` Attach an account-owned phone number to an existing agent. ```yaml operationId: attachPhoneNumber requestBody: required: true content: application/json: schema: type: object required: - phone_number_id - agent_id properties: phone_number_id: type: integer description: ID of the phone number to attach. example: 23 agent_id: type: integer description: ID of the agent to attach the phone number to. example: 158910 responses: '200': description: Phone number attached. content: application/json: schema: type: object properties: phone_number_id: type: integer message: type: string example: phone_number_id: 23 message: Phone Number +15551234567 Attach Successfully. ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # Attach a phone number to an agent phone_number_id = 123 # Replace with your phone number ID agent_id = 456 # Replace with your agent ID response = client.phone_number.attach(phone_number_id, agent_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/attach" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Detach phone number (/docs/api-reference/phone-numbers/detachPhoneNumber) > Detach a phone number from its associated agent. **POST** `/phone_number/detach` Detach a phone number from its associated agent. ```yaml operationId: detachPhoneNumber requestBody: required: true content: application/json: schema: type: object required: - phone_number_id properties: phone_number_id: type: integer description: ID of the phone number to detach. example: 23 responses: '200': description: Phone number detached. content: application/json: schema: type: object properties: phone_number_id: type: integer message: type: string example: phone_number_id: 23 message: Phone Number +15551234567 Detached Successfully. ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # Detach a phone number from its associated agent phone_number_id = 123 # Replace with your phone number ID response = client.phone_number.detach(phone_number_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/detach" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Import Exotel number (/docs/api-reference/phone-numbers/importExotelNumber) > Import an Exotel number by providing your Exotel credentials. **POST** `/phone_number/import/exotel` Import an Exotel number by providing your Exotel credentials. ```yaml operationId: importExotelNumber requestBody: required: true content: application/json: schema: type: object required: - exotel_phone_number - exotel_api_key - exotel_api_token - exotel_subdomain - exotel_account_sid - exotel_app_id properties: exotel_phone_number: type: string description: Exotel phone number in E.164 format. example: '+919876543210' exotel_api_key: type: string description: Your Exotel API key. exotel_api_token: type: string description: Your Exotel API token. exotel_subdomain: type: string description: Your Exotel subdomain (e.g. `your-account.in.exotel.com`). exotel_account_sid: type: string description: Your Exotel account SID. exotel_app_id: type: string description: The Exotel App ID configured for the bot. name: type: string description: Optional friendly name for the imported number. responses: '200': description: Exotel number imported. content: application/json: schema: type: object properties: success: type: boolean message: type: string id: type: integer description: ID of the newly imported phone number record. Use this with `/phone_number/attach`. example: success: true message: Number added successfully id: 3360 ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # Import an existing Exotel number response = client.phone_number.import_exotel_number( exotel_phone_number="02261234567", exotel_api_key="your_exotel_api_key", exotel_api_token="your_exotel_api_token", exotel_subdomain="your_subdomain", exotel_account_sid="your_account_sid", exotel_app_id="your_app_id", name="My Exotel Number" # Optional ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/import/exotel" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Import SIP trunk (/docs/api-reference/phone-numbers/importSipTrunk) > Import a phone number associated with a SIP trunk. **POST** `/phone_number/import/sip` Import a phone number associated with a SIP trunk. ```yaml operationId: importSipTrunk requestBody: required: true content: application/json: schema: type: object required: - phone_number - sip_host - sip_trunk_name properties: phone_number: type: string description: Phone number in E.164 format (starting with `+`). example: '+12025550123' sip_host: type: string description: SIP server hostname or IP. example: sip.yourprovider.com sip_trunk_name: type: string description: Name for this SIP trunk (must be unique within your account). name: type: string description: Optional friendly name for the imported number. sip_port: type: integer default: 5060 description: SIP server port. sip_username: type: string description: SIP authentication username. sip_password: type: string format: password description: SIP authentication password. sip_dial_prefix: type: string description: >- Optional prefix to prepend before the destination number when dialing (e.g. to strip the country code). sip_strip_plus: type: boolean description: When true, strips the leading `+` from the dialed number. responses: '200': description: SIP trunk imported. content: application/json: schema: type: object properties: success: type: boolean message: type: string id: type: integer description: ID of the newly imported phone number record. Use this with `/phone_number/attach`. example: success: true message: SIP trunk registered successfully id: 3360 ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # Import a SIP trunk response = client.phone_number.import_sip_number( phone_number="+1234567890", sip_host="sip.yourprovider.com", sip_trunk_name="my-sip-trunk", name="My SIP Number", # Optional sip_port=5060, # Optional, defaults to 5060 sip_username="user123", # Optional sip_password="secret", # Optional ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/import/sip" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Import Twilio number (/docs/api-reference/phone-numbers/importTwilioNumber) > Import an existing Twilio number by providing your Twilio credentials. **POST** `/phone_number/import/twilio` Import an existing Twilio number by providing your Twilio credentials. ```yaml operationId: importTwilioNumber requestBody: required: true content: application/json: schema: type: object required: - phone_number - account_sid - account_token properties: phone_number: type: string description: Phone number in E.164 format (starting with `+`). example: '+12025550123' account_sid: type: string description: Your Twilio account SID. account_token: type: string description: Your Twilio auth token. name: type: string description: Optional friendly name for the imported number. responses: '200': description: Twilio number imported. content: application/json: schema: type: object properties: success: type: boolean message: type: string id: type: integer description: ID of the newly imported phone number record. Use this with `/phone_number/attach`. example: success: true message: Number added successfully id: 3360 ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # Import an existing Twilio number response = client.phone_number.import_twilio_number( phone_number="+1234567890", account_sid="AC1234567890abcdef1234567890abcdef", account_token="your_twilio_auth_token", name="My Twilio Number" # Optional ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/import/twilio" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List phone numbers (/docs/api-reference/phone-numbers/listPhoneNumbers) > Retrieve the phone numbers on your account, whether you bought them from the OmniDimension number shop or imported your own. **GET** `/phone_number/list` Retrieve the phone numbers on your account, whether you bought them from the OmniDimension number shop or imported your own. ```yaml operationId: listPhoneNumbers parameters: - name: pageno in: query description: Page number for pagination. schema: type: integer default: 1 - name: pagesize in: query description: Items per page (max 150). schema: type: integer default: 30 maximum: 150 - name: user_id in: query required: false description: 'Reseller accounts only: the client to act on. Omit it to act on your own account.' schema: type: integer responses: '200': description: Paginated phone numbers. content: application/json: schema: type: object properties: success: type: boolean phone_numbers: type: array items: type: object description: A phone number on the account. properties: id: type: integer example: 3360 name: type: string user_id: type: integer user_name: type: string phone_number: type: string example: '+15551234567' active_bot_id: oneOf: - type: integer - type: boolean description: ID of the agent currently attached to this number, or `false` if none. purchase_date: type: string example: 05/01/2026 11:04:56 expiry_date: oneOf: - type: string - type: boolean location: type: string number_provider: type: string enum: - twilio - exotel - sip - cloud_whatsapp number_source: type: string is_manually_imported: type: boolean can_message: type: boolean call_sid: oneOf: - type: string - type: boolean session_start_time: oneOf: - type: string - type: 'null' telephony_did_id: oneOf: - type: integer - type: 'null' health_score: oneOf: - type: number - type: 'null' health_score_computed_at: oneOf: - type: string - type: 'null' health_score_details: oneOf: - type: object - type: 'null' sip_host: oneOf: - type: string - type: boolean sip_port: oneOf: - type: string - type: boolean sip_username: oneOf: - type: string - type: boolean sip_trunk_name: oneOf: - type: string - type: boolean sip_id: oneOf: - type: string - type: boolean exotel_phone_number: oneOf: - type: string - type: boolean exotel_subdomain: oneOf: - type: string - type: boolean exotel_account_sid: oneOf: - type: string - type: boolean exotel_app_id: oneOf: - type: string - type: boolean is_phone_wa: type: boolean is_cloud_wa: type: boolean wa_wbaid: oneOf: - type: string - type: boolean wa_app_id: oneOf: - type: string - type: boolean wa_business_id: oneOf: - type: string - type: boolean example: success: true phone_numbers: - id: 213 name: sales-line user_id: 1234 user_name: Demo User phone_number: '+15551234567' can_message: false purchase_date: 04/02/2026 09:24:29 active_bot_id: false location: US number_provider: sip call_sid: false exotel_subdomain: false exotel_account_sid: false exotel_phone_number: false exotel_app_id: false is_phone_wa: false session_start_time: null is_cloud_wa: false wa_wbaid: false wa_app_id: false wa_business_id: false sip_host: sip_port: sip_username: sip_trunk_name: inbound-trunk sip_id: number_source: imported is_manually_imported: false telephony_did_id: null expiry_date: '' health_score: null health_score_computed_at: null health_score_details: null ``` **Python SDK** ```python import os from omnidimension import Client # Initialize the client api_key = os.environ.get('OMNIDIM_API_KEY') client = Client(api_key) # List all phone numbers with pagination response = client.phone_number.list(page=1, page_size=10) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/phone_number/list" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` Buying and releasing numbers is covered in [Buy a number over the API](/docs/buy-a-number-api). # Purchase a phone number (/docs/api-reference/phone-numbers/purchasePhoneNumber) > Buy a phone number from the OmniDimension number shop. The monthly rental comes out of your wallet and the number is added to your account, ready to attach to an agent. **POST** `/phone_number/purchase` Buy a phone number from the OmniDimension number shop. The monthly rental comes out of your wallet and the number is added to your account, ready to attach to an agent. ```yaml operationId: purchasePhoneNumber parameters: - name: Idempotency-Key in: header required: false description: | Your own unique key for this purchase, for example a fresh UUID. Strongly recommended: it is what makes a retry safe. schema: type: string example: 9f2c1d40-7a53-4b8e-9b7a-1c2d3e4f5a6b requestBody: required: true content: application/json: schema: type: object required: - region - phone_number properties: region: type: string enum: - IN - US description: Region the number belongs to. phone_number: type: string description: The number to buy, as returned by the search operation. example: '+15551234567' user_id: type: integer description: 'Reseller accounts only: the client to act on. Omit it to act on your own account.' example: region: US phone_number: '+15551234567' responses: '200': description: | The number was purchased, or the same `Idempotency-Key` was replayed and the original order is returned. content: application/json: schema: type: object properties: success: type: boolean replayed: type: boolean description: Present only when this key was already used. The order was not charged again. order_id: type: integer phone_number: type: string amount: type: number description: Amount charged, in USD. new_balance: type: number description: The owning account's balance after the charge. Not present on a replay. status: type: string description: Always `completed` on a successful purchase. enum: - completed examples: purchased: summary: purchased value: success: true order_id: 5678 phone_number: '+15551234567' amount: 5 new_balance: 120.5 status: completed replayed: summary: replayed value: success: true replayed: true order_id: 5678 phone_number: '+15551234567' amount: 5 status: completed '400': description: A required field is missing, or `phone_number` is not valid. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: invalid_request error_description: Invalid phone number format. '401': description: Missing or invalid API key. '402': description: The client does not have enough balance for this purchase. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: insufficient_balance error_description: Insufficient balance. '403': description: | Phone number access is switched off for the account buying (`feature_disabled`), a `user_id` was sent by a key that is not a reseller admin, or the named client's account is currently unavailable (`forbidden`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: feature_disabled error_description: Phone number access is disabled for this user. '404': description: | The `user_id` did not name one of your clients (`not_found`), or numbers are not available for this region (`not_available`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: not_found error_description: Child user not found. '409': description: | The purchase was refused before anything was charged: identity verification is not complete (`kyc_incomplete`), an earlier purchase is still running (`in_progress`), or the number was taken by someone else (`number_unavailable`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: kyc_incomplete error_description: Complete identity verification for this region first. '422': description: The purchase could not be completed. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: purchase_failed error_description: The purchase could not be completed. Please try again or contact support. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/purchase" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "region": "US", "phone_number": "+15551234567" }' ``` ## Retrying safely [#retrying-safely] A phone number can only be sold once, so this endpoint cannot charge twice for the same number, with or without a key. The `Idempotency-Key` header is for recovering a **lost response**, not for preventing a double sale. Send a fresh UUID with each purchase. If your request times out, retry with the same key and you get your original order back with `replayed: true` instead of guessing whether it went through, or `409 in_progress` if it is still running. Reusing a key from a different purchase returns that first order. It is not validated against your new request body, so use a fresh key each time. ## Before you can buy [#before-you-can-buy] Some regions require the account to be identity-verified first. India is one, and the check is an Aadhaar eKYC, so it completes in minutes. Verification done in the dashboard counts: it is the same gate either way. The whole flow, end to end, is in [Buy a number over the API](/docs/buy-a-number-api). # Release a phone number (/docs/api-reference/phone-numbers/releasePhoneNumber) > Give up a phone number and stop its rental, so it is not charged at the next renewal. Only a number currently allocated to the account can be released. **POST** `/phone_number/release` Give up a phone number and stop its rental, so it is not charged at the next renewal. Only a number currently allocated to the account can be released. ```yaml operationId: releasePhoneNumber requestBody: required: true content: application/json: schema: type: object required: - phone_number properties: phone_number: type: string description: The number to release. example: '+15551234567' user_id: type: integer description: 'Reseller accounts only: the client to act on. Omit it to act on your own account.' example: phone_number: '+15551234567' responses: '200': description: The number was released. content: application/json: schema: type: object properties: success: type: boolean phone_number: type: string status: type: string enum: - released example: success: true phone_number: '+15551234567' status: released '400': description: The request body is missing a required field. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: invalid_request error_description: 'Missing required fields: phone_number.' '401': description: Missing or invalid API key. '403': description: | A `user_id` was sent by a key that is not a reseller admin, or the named client's account is currently unavailable. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: forbidden error_description: Access denied. Only reseller accounts can use this endpoint. '404': description: | The `user_id` did not name one of your clients (`not_found`), or the account holds no such number (`number_not_found`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: number_not_found error_description: No such number on this user. '422': description: The number could not be released. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: release_failed error_description: The number could not be released. Please try again or contact support. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/phone_number/release" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+15551234567" }' ``` Releasing gives the number up and stops its rental, so it is not charged at the next renewal. More in [Buy a number over the API](/docs/buy-a-number-api). # Search available phone numbers (/docs/api-reference/phone-numbers/searchPhoneNumbers) > Search the OmniDimension number shop for phone numbers available to buy in a region. Price and validity are flat per region, so every result shows the same monthly_rental_usd and validity_days, and that is the exact amount a purchase will charge. **GET** `/phone_number/search` Search the OmniDimension number shop for phone numbers available to buy in a region. Price and validity are flat per region, so every result shows the same `monthly_rental_usd` and `validity_days`, and that is the exact amount a purchase will charge. ```yaml operationId: searchPhoneNumbers parameters: - name: region in: query required: true description: Region to search in. schema: type: string enum: - IN - US example: US - name: pattern in: query required: false description: Digits or prefix to match within the number. schema: type: string example: '555' - name: page in: query required: false description: Page of results to return. schema: type: integer minimum: 1 default: 1 - name: limit in: query required: false description: Results per page. schema: type: integer minimum: 1 maximum: 150 default: 20 - name: user_id in: query required: false description: 'Reseller accounts only: the client to act on. Omit it to act on your own account.' schema: type: integer responses: '200': description: Numbers available in this region. content: application/json: schema: type: object properties: success: type: boolean region: type: string enum: - IN - US numbers: type: array items: type: object properties: phone_number: type: string monthly_rental_usd: type: number description: Amount, in USD, that a purchase of this number will charge per month. validity_days: type: integer description: Days the number stays active before it has to be renewed. region: type: string enum: - IN - US kyc_required: type: boolean description: Whether identity verification is required before buying this number. total: type: integer page: type: integer limit: type: integer total_pages: type: integer example: success: true region: US numbers: - phone_number: '+15551234567' monthly_rental_usd: 5 validity_days: 30 region: US kyc_required: false total: 1 page: 1 limit: 20 total_pages: 1 '400': description: '`page` or `limit` is not a valid integer, or is out of range.' content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: invalid_request error_description: page must be >= 1 and limit between 1 and 150. '401': description: Missing or invalid API key. '403': description: | A `user_id` was sent by a key that is not a reseller admin, or the named client's account is currently unavailable. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: forbidden error_description: Access denied. Only reseller accounts can use this endpoint. '404': description: | The `user_id` did not name one of your clients (`not_found`), or numbers are not available for this region (`not_available`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: not_found error_description: Child user not found. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/phone_number/search?region=US&pattern=555" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` Searching does not reserve anything. A number is claimed only when a purchase starts, so a number you saw a moment ago can be gone by the time you buy it. Next step: [purchase a number](/docs/api-reference/phone-numbers/purchasePhoneNumber), or read the flow end to end in [Buy a number over the API](/docs/buy-a-number-api). # Get voice details (/docs/api-reference/providers/getVoice) > Detailed metadata for a specific voice. **GET** `/providers/voice/{voice_id}` Detailed metadata for a specific voice. ```yaml operationId: getVoice parameters: - name: voice_id in: path required: true schema: type: integer description: Numeric id of the voice, as returned in the `id` field of the voices list. responses: '200': description: Voice details. content: application/json: schema: type: object description: A single voice's metadata. properties: id: oneOf: - type: integer - type: 'null' name: type: string display_name: type: string service: type: string sample_url: type: string format: uri example: id: 1 name: aura-luna-en display_name: luna service: deepgram sample_url: https://res.cloudinary.com/deepgram/video/upload/v1709565351/aura/luna_docs_clom0e.wav '404': description: No voice with that ID. content: application/json: schema: type: object properties: error: type: string example: Voice not found ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get detailed information about a specific voice voice_details = client.providers.get_voice(123) print(voice_details) # Returns: {'id': 123, 'name': 'Voice Name', 'provider': 'eleven_labs', ...} ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/voice/{voice_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List all providers (/docs/api-reference/providers/listAllProviders) > Comprehensive response with services and voices in one payload. **GET** `/providers/all` Comprehensive response with services and voices in one payload. ```yaml operationId: listAllProviders responses: '200': description: Comprehensive providers list. content: application/json: example: services: LLM: - id: 8 name: gemini-2.5-flash display_name: gemini-2.5-flash service_type: LLM - id: 44 name: gpt-5.4 display_name: gpt-5.4 service_type: LLM - id: 6 name: gemini-1.5-pro display_name: gemini-1.5-pro service_type: LLM - id: 7 name: gemini-2.0-flash display_name: gemini-2.0-flash service_type: LLM - id: 9 name: gemini-2.5-pro display_name: gemini-2.5-pro service_type: LLM - id: 10 name: gemini-2.5-flash-lite display_name: gemini-2.5-flash-lite service_type: LLM - id: 11 name: gpt-4o display_name: gpt-4o service_type: LLM - id: 12 name: gpt-4o-mini display_name: gpt-4o-mini service_type: LLM - id: 13 name: gpt-3.5-turbo display_name: gpt-3.5-turbo service_type: LLM - id: 14 name: azure-gpt-4o display_name: azure-gpt-4o service_type: LLM - id: 15 name: azure-gpt-4o-mini display_name: azure-gpt-4o-mini service_type: LLM - id: 16 name: azure-gpt-4.1-nano display_name: azure-gpt-4.1-nano service_type: LLM - id: 17 name: azure-gpt-4.1-mini display_name: azure-gpt-4.1-mini service_type: LLM - id: 18 name: llama3-8b-8192 display_name: llama3-8b-8192 service_type: LLM - id: 19 name: llama3-70b-8192 display_name: llama3-70b-8192 service_type: LLM - id: 20 name: llama-3.3-70b-versatile display_name: llama-3.3-70b-versatile service_type: LLM - id: 21 name: gemma2-9b-it display_name: gemma2-9b-it service_type: LLM - id: 22 name: claude-opus-4-0 display_name: claude-opus-4-0 service_type: LLM - id: 23 name: claude-sonnet-4-0 display_name: claude-sonnet-4-0 service_type: LLM - id: 24 name: claude-3-7-sonnet-latest display_name: claude-3-7-sonnet-latest service_type: LLM - id: 25 name: claude-3-5-sonnet-latest display_name: claude-3-5-sonnet-latest service_type: LLM - id: 26 name: claude-3-5-haiku-latest display_name: claude-3-5-haiku-latest service_type: LLM - id: 27 name: claude-3-opus-latest display_name: claude-3-opus-latest service_type: LLM - id: 28 name: Your own LLM display_name: Your own LLM service_type: LLM - id: 39 name: gemini-3-flash-preview display_name: gemini-3-flash-preview service_type: LLM - id: 40 name: gemini-3-pro-preview display_name: gemini-3-pro-preview service_type: LLM TTS: - id: 41 name: timepay display_name: timepay service_type: TTS - id: 29 name: deepgram display_name: deepgram service_type: TTS - id: 30 name: google display_name: google service_type: TTS - id: 31 name: eleven_labs display_name: eleven_labs service_type: TTS - id: 32 name: cartesia display_name: cartesia service_type: TTS - id: 37 name: sarvam display_name: sarvam service_type: TTS - id: 35 name: hume display_name: hume service_type: TTS - id: 33 name: rime display_name: rime service_type: TTS - id: 38 name: openAI display_name: openAI service_type: TTS - id: 34 name: playht display_name: playht service_type: TTS - id: 36 name: inworld display_name: inworld service_type: TTS STT: - id: 42 name: Soniox display_name: Soniox service_type: STT - id: 43 name: Speechmatics display_name: Speechmatics service_type: STT - id: 1 name: whisper display_name: whisper service_type: STT - id: 2 name: deepgram_stream display_name: deepgram_stream service_type: STT - id: 3 name: Cartesia display_name: Cartesia service_type: STT - id: 4 name: Sarvam display_name: Sarvam service_type: STT - id: 5 name: Azure display_name: Azure service_type: STT voices: - id: 1 name: aura-luna-en display_name: luna service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565351/aura/luna_docs_clom0e.wav - id: 2 name: aura-stella-en display_name: stella service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565349/aura/stella_docs_xh5jbv.wav - id: 3 name: en-IN-Neural2-A display_name: Neural2 A (Female) service: google sample_url: '' - id: 4 name: aura-asteria-en display_name: asteria service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565353/aura/asteria_docs_venw0r.wav - id: 5 name: aura-hera-en display_name: hera service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565347/aura/hera_docs_xjkt4x.wav - id: 7 name: en-IN-Journey-O display_name: Journey (IN) service: google sample_url: http://backend.omnidim.io/web/content/bot.voice/7/sample_audio - id: 8 name: cgSgspJ2msm6clMCkdW9 display_name: Jessica service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/cgSgspJ2msm6clMCkdW9/56a97bf8-b69b-448f-846c-c3a11683d45a.mp3 - id: 9 name: en-US-Journey-F display_name: Journey(US-Female) service: google sample_url: false - id: 10 name: aura-athena-en display_name: athena service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565613/aura/athena_docs_wyznud.wav - id: 11 name: aura-orion-en display_name: orion service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565346/aura/orion_docs_aljv1q.mp3 - id: 12 name: aura-arcas-en display_name: arcas service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565348/aura/arcas_docs_pc9hxp.mp3 - id: 13 name: aura-perseus-en display_name: perseus service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565350/aura/perseus_docs_ap7fc0.wav - id: 14 name: aura-angus-en display_name: angus service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565352/aura/angus_docs_lgse2b.wav - id: 15 name: aura-orpheus-en display_name: orpheus service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565350/aura/orpheus_docs_zdlpcf.wav - id: 16 name: aura-helios-en display_name: helios service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565346/aura/helios_docs_ycjwoo.wav - id: 17 name: aura-zeus-en display_name: zeus service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565347/aura/zeus_docs_fupdiv.wav - id: 32 name: 9BWtsMINqrJLrRacOk9x display_name: Aria service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/9BWtsMINqrJLrRacOk9x/405766b8-1f4e-4d3c-aba1-6f25333823ec.mp3 - id: 33 name: CwhRBWXzGAHq8TQ4Fs17 display_name: Roger service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/CwhRBWXzGAHq8TQ4Fs17/58ee3ff5-f6f2-4628-93b8-e38eb31806b0.mp3 - id: 34 name: EXAVITQu4vr4xnSDxMaL display_name: Sarah service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/EXAVITQu4vr4xnSDxMaL/01a3e33c-6e99-4ee7-8543-ff2216a32186.mp3 - id: 35 name: FGY2WhTYpPnrIDTdsKH5 display_name: Laura service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/FGY2WhTYpPnrIDTdsKH5/67341759-ad08-41a5-be6e-de12fe448618.mp3 - id: 36 name: IKne3meq5aSn9XLyUdCD display_name: Charlie service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/IKne3meq5aSn9XLyUdCD/102de6f2-22ed-43e0-a1f1-111fa75c5481.mp3 - id: 37 name: JBFqnCBsd6RMkjVDRZzb display_name: George service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/JBFqnCBsd6RMkjVDRZzb/e6206d1a-0721-4787-aafb-06a6e705cac5.mp3 - id: 38 name: N2lVS1w4EtoT3dr4eOWO display_name: Callum service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/N2lVS1w4EtoT3dr4eOWO/ac833bd8-ffda-4938-9ebc-b0f99ca25481.mp3 - id: 39 name: SAz9YHcvj6GT2YYXdXww display_name: River service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/SAz9YHcvj6GT2YYXdXww/e6c95f0b-2227-491a-b3d7-2249240decb7.mp3 - id: 40 name: TX3LPaxmHKxFdv7VOQHJ display_name: Liam service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/TX3LPaxmHKxFdv7VOQHJ/63148076-6363-42db-aea8-31424308b92c.mp3 - id: 41 name: XB0fDUnXU5powFXDhCwa display_name: Charlotte service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/XB0fDUnXU5powFXDhCwa/942356dc-f10d-4d89-bda5-4f8505ee038b.mp3 - id: 42 name: Xb7hH8MSUJpSbSDYk0k2 display_name: Alice service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/Xb7hH8MSUJpSbSDYk0k2/d10f7534-11f6-41fe-a012-2de1e482d336.mp3 - id: 43 name: XrExE9yKIg1WjnnlVkGX display_name: Matilda service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/XrExE9yKIg1WjnnlVkGX/b930e18d-6b4d-466e-bab2-0ae97c6d8535.mp3 - id: 44 name: bIHbv24MWmeRgasZH58o display_name: Will service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/bIHbv24MWmeRgasZH58o/8caf8f3d-ad29-4980-af41-53f20c72d7a4.mp3 - id: 45 name: cjVigY5qzO86Huf0OWal display_name: Eric service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/cjVigY5qzO86Huf0OWal/d098fda0-6456-4030-b3d8-63aa048c9070.mp3 - id: 46 name: iP95p4xoKVk53GoZ742B display_name: Chris service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/iP95p4xoKVk53GoZ742B/3f4bde72-cc48-40dd-829f-57fbf906f4d7.mp3 - id: 47 name: nPczCjzI2devNBz1zQrb display_name: Brian service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/nPczCjzI2devNBz1zQrb/2dd3e72c-4fd3-42f1-93ea-abc5d4e5aa1d.mp3 - id: 48 name: onwK4e9ZLuTAKqWW03F9 display_name: Daniel service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/onwK4e9ZLuTAKqWW03F9/7eee0236-1a72-4b86-b303-5dcadc007ba9.mp3 - id: 49 name: pFZP5JQG7iQjIQuC4Bku display_name: Lily service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/pFZP5JQG7iQjIQuC4Bku/89b68b35-b3dd-4348-a84a-a3c13a3c2b30.mp3 - id: 50 name: pqHfZKP75CvOlQylNhV4 display_name: Bill service: eleven_labs sample_url: >- https://storage.googleapis.com/eleven-public-prod/premade/voices/pqHfZKP75CvOlQylNhV4/d782b3ff-84ba-4029-848c-acf01285524d.mp3 - id: 51 name: anushka display_name: Anushka service: sarvam sample_url: false - id: 52 name: manisha display_name: Manisha service: sarvam sample_url: false - id: 53 name: vidya display_name: Vidya service: sarvam sample_url: false - id: 54 name: arya display_name: Arya service: sarvam sample_url: false - id: 55 name: abhilash display_name: Abhilash service: sarvam sample_url: false - id: 56 name: karun display_name: Karun service: sarvam sample_url: false - id: 57 name: hitesh display_name: Hitesh service: sarvam sample_url: false - id: 58 name: en-IN-Chirp-HD-D display_name: Chirp HD D (Male) service: google sample_url: false - id: 59 name: en-IN-Chirp-HD-F display_name: Chirp HD F (Female) service: google sample_url: false - id: 60 name: en-IN-Chirp-HD-O display_name: Chirp HD O (Female) service: google sample_url: false - id: 61 name: en-IN-Chirp3-HD-Achernar display_name: Achernar (Female) service: google sample_url: false - id: 62 name: en-IN-Chirp3-HD-Achird display_name: Achird (Male) service: google sample_url: false - id: 63 name: en-IN-Chirp3-HD-Algenib display_name: Algenib (Male) service: google sample_url: false - id: 64 name: en-IN-Chirp3-HD-Algieba display_name: Algieba (Male) service: google sample_url: false - id: 65 name: en-IN-Chirp3-HD-Alnilam display_name: Alnilam (Male) service: google sample_url: false - id: 66 name: en-IN-Chirp3-HD-Aoede display_name: Aoede (Female) service: google sample_url: false - id: 67 name: en-IN-Chirp3-HD-Autonoe display_name: Autonoe (Female) service: google sample_url: false - id: 68 name: en-IN-Chirp3-HD-Callirrhoe display_name: Callirrhoe (Female) service: google sample_url: false - id: 69 name: en-IN-Chirp3-HD-Charon display_name: Charon (Male) service: google sample_url: false - id: 70 name: en-IN-Chirp3-HD-Despina display_name: Despina (Female) service: google sample_url: false - id: 71 name: en-IN-Chirp3-HD-Enceladus display_name: Enceladus (Male) service: google sample_url: false - id: 72 name: en-IN-Chirp3-HD-Erinome display_name: Erinome (Female) service: google sample_url: false - id: 73 name: en-IN-Chirp3-HD-Fenrir display_name: Fenrir (Male) service: google sample_url: false - id: 74 name: en-IN-Chirp3-HD-Gacrux display_name: Gacrux (Female) service: google sample_url: false - id: 75 name: en-IN-Chirp3-HD-Iapetus display_name: Iapetus (Male) service: google sample_url: false - id: 76 name: en-IN-Chirp3-HD-Kore display_name: Kore (Female) service: google sample_url: false - id: 77 name: en-IN-Chirp3-HD-Laomedeia display_name: Laomedeia (Female) service: google sample_url: false - id: 78 name: en-IN-Chirp3-HD-Leda display_name: Leda (Female) service: google sample_url: false - id: 79 name: en-IN-Chirp3-HD-Orus display_name: Orus (Male) service: google sample_url: false - id: 80 name: en-IN-Chirp3-HD-Puck display_name: Puck (Male) service: google sample_url: false - id: 81 name: en-IN-Chirp3-HD-Pulcherrima display_name: Pulcherrima (Female) service: google sample_url: false - id: 82 name: en-IN-Chirp3-HD-Rasalgethi display_name: Rasalgethi (Male) service: google sample_url: false - id: 83 name: en-IN-Chirp3-HD-Sadachbia display_name: Sadachbia (Male) service: google sample_url: false - id: 84 name: en-IN-Chirp3-HD-Sadaltager display_name: Sadaltager (Male) service: google sample_url: false - id: 85 name: en-IN-Chirp3-HD-Schedar display_name: Schedar (Male) service: google sample_url: false - id: 86 name: en-IN-Chirp3-HD-Sulafat display_name: Sulafat (Female) service: google sample_url: false - id: 87 name: en-IN-Chirp3-HD-Umbriel display_name: Umbriel (Male) service: google sample_url: false - id: 88 name: en-IN-Chirp3-HD-Vindemiatrix display_name: Vindemiatrix (Female) service: google sample_url: false - id: 89 name: en-IN-Chirp3-HD-Zephyr display_name: Zephyr (Female) service: google sample_url: false - id: 90 name: en-IN-Chirp3-HD-Zubenelgenubi display_name: Zubenelgenubi (Male) service: google sample_url: false - id: 91 name: en-IN-Neural2-B display_name: Neural2 B (Male) service: google sample_url: false - id: 92 name: en-IN-Neural2-C display_name: Neural2 C (Male) service: google sample_url: false - id: 93 name: en-IN-Neural2-D display_name: Neural2 D (Female) service: google sample_url: false - id: 94 name: en-IN-Standard-A display_name: Standard A (Female) service: google sample_url: false - id: 95 name: en-IN-Standard-B display_name: Standard B (Male) service: google sample_url: false - id: 96 name: en-IN-Standard-C display_name: Standard C (Male) service: google sample_url: false - id: 97 name: en-IN-Standard-D display_name: Standard D (Female) service: google sample_url: false - id: 98 name: en-IN-Standard-E display_name: Standard E (Female) service: google sample_url: false - id: 99 name: en-IN-Standard-F display_name: Standard F (Male) service: google sample_url: false - id: 100 name: en-IN-Wavenet-A display_name: Wavenet A (Female) service: google sample_url: false - id: 101 name: en-IN-Wavenet-B display_name: Wavenet B (Male) service: google sample_url: false - id: 102 name: en-IN-Wavenet-C display_name: Wavenet C (Male) service: google sample_url: false - id: 103 name: en-IN-Wavenet-D display_name: Wavenet D (Female) service: google sample_url: false - id: 104 name: en-IN-Wavenet-E display_name: Wavenet E (Female) service: google sample_url: false - id: 105 name: en-IN-Wavenet-F display_name: Wavenet F (Male) service: google sample_url: false - id: 172 name: f8f5f1b2-f02d-4d8e-a40d-fd850a487b3d display_name: Kiara - Joyful Woman service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/172/sample_audio - id: 173 name: d7e54830-4754-4b17-952c-bcdb7e80a2fb display_name: Mabel - Grandma service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/173/sample_audio - id: 174 name: e00d0e4c-a5c8-443f-a8a3-473eb9a62355 display_name: Zeke - Friendly Sidekick service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/174/sample_audio - id: 175 name: 42b39f37-515f-4eee-8546-73e841679c1d display_name: James - Navigator service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/175/sample_audio - id: 176 name: a38e4e85-e815-43ab-acf1-907c4688dd6c display_name: Lindsey - Relaxed Rep service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/176/sample_audio - id: 177 name: 41534e16-2966-4c6b-9670-111411def906 display_name: Clarence - Newsman service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/177/sample_audio - id: 178 name: f31cc6a7-c1e8-4764-980c-60a361443dd1 display_name: Olivia - Sunny Woman service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/178/sample_audio - id: 179 name: 21b81c14-f85b-436d-aff5-43f2e788ecf8 display_name: Riley - Chill Friend service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/179/sample_audio - id: 180 name: 1259b7e3-cb8a-43df-9446-30971a46b8b0 display_name: Devansh - Warm Support Agent service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/180/sample_audio - id: 181 name: 9cebb910-d4b7-4a4a-85a4-12c79137724c display_name: Aarti - Conversationalist service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/181/sample_audio - id: 182 name: 4df027cb-2920-4a1f-8c34-f21529d5c3fe display_name: Carson - Friendly Support service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/182/sample_audio - id: 183 name: 0418348a-0ca2-4e90-9986-800fb8b3bbc0 display_name: Antoine - Stern Man service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/183/sample_audio - id: 184 name: 1fc31370-81b1-4588-9c1a-f93793c6e01d display_name: Carlo - Roman Guide service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/184/sample_audio - id: 185 name: 87bc56aa-ab01-4baa-9071-77d497064686 display_name: Jordan - Chill Pal service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/185/sample_audio - id: 186 name: bec003e2-3cb3-429c-8468-206a393c67ad display_name: Parvati - Friendly Supporter service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/186/sample_audio - id: 187 name: f6141af3-5f94-418c-80ed-a45d450e7e2e display_name: Priya - Trusted Operator service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/187/sample_audio - id: 188 name: 8985388c-1332-4ce7-8d55-789628aa3df4 display_name: Robyn - Storycrafter service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/188/sample_audio - id: 189 name: 79743797-2087-422f-8dc7-86f9efca85f1 display_name: Fran - Confident Young Professional service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/189/sample_audio - id: 190 name: d718e944-b313-4998-b011-d1cc078d4ef3 display_name: Liv - Casual Friend service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/190/sample_audio - id: 191 name: 043cfc81-d69f-4bee-ae1e-7862cb358650 display_name: Amelia - Instructor service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/191/sample_audio - id: 192 name: 1d3ba41a-96e6-44ad-aabb-9817c56caa68 display_name: Mia - Agent service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/192/sample_audio - id: 193 name: 38aabb6a-f52b-4fb0-a3d1-988518f4dc06 display_name: Alina - Engaging Assistant service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/193/sample_audio - id: 194 name: 82a7fc13-2927-4e42-9b8a-bb1f9e506521 display_name: Tomek - Casual Companion service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/194/sample_audio - id: 195 name: c8605446-247c-4d39-acd4-8f4c28aa363c display_name: Edith - Matriarch service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/195/sample_audio - id: 196 name: f114a467-c40a-4db8-964d-aaba89cd08fa display_name: Miles - Yogi service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/196/sample_audio - id: 197 name: 607167f6-9bf2-473c-accc-ac7b3b66b30b display_name: Brenda - Host service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/197/sample_audio - id: 198 name: 064b17af-d36b-4bfb-b003-be07dba1b649 display_name: Tatiana - Friendly Storyteller service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/198/sample_audio - id: 199 name: cccc21e8-5bcf-4ff0-bc7f-be4e40afc544 display_name: Avery - Gaming Girl service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/199/sample_audio - id: 200 name: c0c374aa-09be-42d9-9828-4d2d7df86962 display_name: Isabel - Teacher service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/200/sample_audio - id: 201 name: 55deba52-bc73-4481-ab69-9c8831c8a7c3 display_name: Camille - Friendly Expert service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/201/sample_audio - id: 202 name: bd9120b6-7761-47a6-a446-77ca49132781 display_name: Owen - Tutorial Man service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/202/sample_audio - id: 203 name: 701a96e1-7fdd-4a6c-a81e-a4a450403599 display_name: Rowan - Team Leader service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/203/sample_audio - id: 204 name: 1cf751f6-8749-43ab-98bd-230dd633abdb display_name: Ana Paula - Marketer service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/204/sample_audio - id: 205 name: 3f4ade23-6eb4-4279-ab05-6a144947c4d5 display_name: Karin - Companion service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/205/sample_audio - id: 206 name: 8832a0b5-47b2-4751-bb22-6a8e2149303d display_name: French Narrator Lady service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/206/sample_audio - id: 207 name: 3e1ed423-17e5-4773-b87c-25b031106e41 display_name: Paul - Straight Talker service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/207/sample_audio - id: 208 name: 2695b6b5-5543-4be1-96d9-3967fb5e7fec display_name: Agustin - Clear Storyteller service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/208/sample_audio - id: 209 name: 700d1ee3-a641-4018-ba6e-899dcadc9e2b display_name: Luana - Public Speaker service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/209/sample_audio - id: 210 name: 996a8b96-4804-46f0-8e05-3fd4ef1a87cd display_name: Darla - Resolution Agent service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/210/sample_audio - id: 211 name: da4a4eff-3b7e-4846-8f70-f075ff61222c display_name: Callum - Brand Spokesperson service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/211/sample_audio - id: 212 name: fb26447f-308b-471e-8b00-8e9f04284eb5 display_name: Thistle - Troublemaker service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/212/sample_audio - id: 213 name: bf991597-6c13-47e4-8411-91ec2de5c466 display_name: Carol - Task Coach service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/213/sample_audio - id: 214 name: 5c3c89e5-535f-43ef-b14d-f8ffe148c1f0 display_name: French Narrator Man service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/214/sample_audio - id: 215 name: ab7c61f5-3daa-47dd-a23b-4ac0aac5f5c3 display_name: Friendly French Man service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/215/sample_audio - id: 216 name: f91ab3e6-5071-4e15-b016-cde6f2bcd222 display_name: Aadhya - Soother service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/216/sample_audio - id: 217 name: e07c00bc-4134-4eae-9ea4-1a55fb45746b display_name: Brooke - Big Sister service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/217/sample_audio - id: 218 name: 228fca29-3a0a-435c-8728-5cb483251068 display_name: Kiefer service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/218/sample_audio - id: 219 name: ec1e269e-9ca0-402f-8a18-58e0e022355a display_name: Ariana service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/219/sample_audio - id: 220 name: 5cad89c9-d88a-4832-89fb-55f2f16d13d3 display_name: Brandon service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/220/sample_audio - id: 221 name: 03496517-369a-4db1-8236-3d3ae459ddf7 display_name: Calypso - ASMR Lady service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/221/sample_audio - id: 222 name: e8e5fffb-252c-436d-b842-8879b84445b6 display_name: Cathy - Coworker service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/222/sample_audio - id: 223 name: f786b574-daa5-4673-aa0c-cbe3e8534c02 display_name: Katie - Friendly Fixer service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/223/sample_audio - id: 224 name: 6ccbfb76-1fc6-48f7-b71d-91ac6298247b display_name: Tessa service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/224/sample_audio - id: 225 name: 66c6b81c-ddb7-4892-bdd5-19b5a7be38e7 display_name: Dorothy service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/225/sample_audio - id: 226 name: w99aEsEd2KhfXwCmjAn4 display_name: Jordan - Kind and Friendly service: eleven_labs sample_url: false - id: 227 name: 2b568345-1d48-4047-b25f-7baccf842eb0 display_name: Yumiko - Friendly Agent service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/227/sample_audio - id: 228 name: 498e7f37-7fa3-4e2c-b8e2-8b6e9276f956 display_name: Aiko - Calming Voice service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/228/sample_audio - id: 229 name: 31c55968-a9f4-4115-8831-3a16952179c8 display_name: Ayumi - Sales Guide service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/229/sample_audio - id: 230 name: 62ae83ad-4f6a-430b-af41-a9bede9286ca display_name: Gemma - Decisive Agent service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/230/sample_audio - id: 231 name: 2f251ac3-89a9-4a77-a452-704b474ccd01 display_name: Lucy - Capable Coordinator service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/231/sample_audio - id: 232 name: 97e7d7a9-dfaa-4758-a936-f5f844ac34cc display_name: Fuji - Positive Colleague service: cartesia sample_url: >- https://192d-2401-4900-8f60-ac89-8959-6836-5aed-73c7.ngrok-free.app/web/content/bot.voice/232/sample_audio - id: 233 name: 9e7ef2cf-b69c-46ac-9e35-bbfd73ba82af display_name: Ren - High-Energy Character service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/233/sample_audio - id: 234 name: a759ecc5-ac21-487e-88c7-288bdfe76999 display_name: Daichi - Baritone Narrator service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/234/sample_audio - id: 235 name: 7c58f4a4-a72c-42fa-a503-41b9408820f3 display_name: "Inès\t- Poised Communicator" service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/235/sample_audio - id: 236 name: 791d5162-d5eb-40f0-8189-f19db44611d8 display_name: Ayush - Friendly Neighbor service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/236/sample_audio - id: 237 name: db6b0ed5-d5d3-463d-ae85-518a07d3c2b4 display_name: Skylar - Friendly Guide service: cartesia sample_url: http://localhost:8069/web/content/bot.voice/237/sample_audio total_services: 44 total_voices: 156 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all providers (comprehensive - services and voices) all_providers = client.providers.list_all() print(all_providers) # Returns: {'services': {...}, 'voices': {...}, 'total_services': 12, 'total_voices': 123} ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/all" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List LLM providers (/docs/api-reference/providers/listLLMProviders) > Retrieve all available Large Language Model providers. **GET** `/providers/llms` Retrieve all available Large Language Model providers. ```yaml operationId: listLLMProviders responses: '200': description: LLM providers. content: application/json: schema: type: object properties: llms: type: array items: type: object properties: id: type: integer name: type: string provider_name: oneOf: - type: string - type: boolean description: Brand of the underlying service (e.g. `azure` for LLMs). `false` when not set. is_premium: type: boolean service_type: type: string enum: - LLM - STT - TTS - other is_active: type: boolean total: type: integer example: llms: - id: 17 name: azure-gpt-4.1-mini provider_name: azure is_premium: false service_type: LLM is_active: true - id: 8 name: gemini-2.5-flash provider_name: google is_premium: false service_type: LLM is_active: true total: 13 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all LLM providers llms = client.providers.list_llms() print(llms) # Returns: {'llms': [...], 'total': 5} ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/llms" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List STT providers (/docs/api-reference/providers/listSTTProviders) > Retrieve all Speech-to-Text providers. **GET** `/providers/stt` Retrieve all Speech-to-Text providers. ```yaml operationId: listSTTProviders responses: '200': description: STT providers. content: application/json: schema: type: object properties: stt: type: array items: type: object properties: id: type: integer name: type: string provider_name: oneOf: - type: string - type: boolean description: Brand of the underlying service (e.g. `azure` for LLMs). `false` when not set. is_premium: type: boolean service_type: type: string enum: - LLM - STT - TTS - other is_active: type: boolean total: type: integer example: stt: - id: 2 name: deepgram_stream provider_name: false is_premium: false service_type: STT is_active: true - id: 5 name: Azure provider_name: false is_premium: false service_type: STT is_active: true total: 4 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all STT providers stt_providers = client.providers.list_stt() print(stt_providers) # Returns: {'stt': [...], 'total': 3} ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/stt" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List TTS providers (/docs/api-reference/providers/listTTSProviders) > Retrieve all Text-to-Speech providers. **GET** `/providers/tts` Retrieve all Text-to-Speech providers. ```yaml operationId: listTTSProviders responses: '200': description: TTS providers. content: application/json: schema: type: object properties: tts: type: array items: type: object properties: id: type: integer name: type: string provider_name: oneOf: - type: string - type: boolean description: Brand of the underlying service (e.g. `azure` for LLMs). `false` when not set. is_premium: type: boolean service_type: type: string enum: - LLM - STT - TTS - other is_active: type: boolean total: type: integer example: tts: - id: 32 name: cartesia provider_name: false is_premium: false service_type: TTS is_active: true - id: 30 name: google provider_name: false is_premium: false service_type: TTS is_active: true total: 4 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all TTS providers tts_providers = client.providers.list_tts() print(tts_providers) # Returns: {'tts': [...], 'total': 4} ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/tts" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List voices (/docs/api-reference/providers/listVoices) > Retrieve voices with filtering and pagination support. ElevenLabs supports advanced filtering by name, language, accent, and gender. Other providers support basic pagination only. **GET** `/providers/voices` Retrieve voices with filtering and pagination support. ElevenLabs supports advanced filtering by name, language, accent, and gender. Other providers support basic pagination only. ```yaml operationId: listVoices parameters: - name: provider in: query description: TTS provider to list voices from. Omit to list across all providers. schema: type: string enum: - eleven_labs - google - deepgram - cartesia - sarvam - name: search in: query description: Substring match against voice name or description. ElevenLabs only. schema: type: string - name: language in: query description: ISO language code (e.g. `en`, `hi`, `es`). ElevenLabs only. schema: type: string - name: accent in: query description: Accent label (e.g. `american`, `british`). ElevenLabs only. schema: type: string - name: gender in: query description: Filter voices by gender. ElevenLabs only. schema: type: string enum: - male - female - name: page in: query description: 1-indexed page number. schema: type: integer default: 1 - name: page_size in: query description: Voices per page. Capped at 100. schema: type: integer default: 30 maximum: 100 responses: '200': description: Voices and filter metadata. content: application/json: schema: type: object properties: voices: type: array items: type: object properties: id: oneOf: - type: integer - type: 'null' name: type: string display_name: type: string service: type: string sample_url: type: string format: uri tags: type: array items: type: string source: type: string description: Origin marker (e.g. `external` for ElevenLabs community voices). external_id: type: string description: Identifier on the upstream provider. description: type: string total: type: integer page: type: integer page_size: type: integer filters_applied: type: object description: >- Echoes the filter parameters the request applied. `null` for any filter that wasn't sent. properties: provider: type: string nullable: true search: type: string nullable: true language: type: string nullable: true accent: type: string nullable: true gender: type: string nullable: true example: voices: - id: 1 name: aura-luna-en display_name: luna service: deepgram sample_url: >- https://res.cloudinary.com/deepgram/video/upload/v1709565351/aura/luna_docs_clom0e.wav tags: - feminine - Young Adult - en-us - American - Friendly, Natural, Engaging - IVR total: 30 page: 1 page_size: 30 filters_applied: provider: null search: null language: null accent: null gender: null ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Basic usage - list all voices with default pagination voices = client.providers.list_voices() print(voices) # Returns: {'voices': [...], 'total': 123, 'page': 1, 'page_size': 30, 'filters_applied': False} # With pagination voices = client.providers.list_voices(page=2, page_size=50) print(voices) # Filter by specific provider (only supports basic pagination) voices = client.providers.list_voices(provider='eleven_labs', page=1, page_size=20) print(voices) # Advanced filtering (ElevenLabs only) voices = client.providers.list_voices( provider='eleven_labs', search='professional', language='en', accent='american', gender='male', page=1, page_size=15 ) print(voices) # Search by voice name/description voices = client.providers.list_voices( provider='eleven_labs', search='excited', gender='female' ) print(voices) # Filter by language voices = client.providers.list_voices( provider='eleven_labs', language='en' ) print(voices) # Filter by accent voices = client.providers.list_voices( provider='eleven_labs', accent='british' ) print(voices) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/providers/voices" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Create session (/docs/api-reference/sessions/createSession) > Create a voice Session: a short-lived, single-conversation reservation that lets a client hold a live voice chat with your agent. This is step 1 of 2. Creating the Session does not start any audio on its own; it returns a ws_url that a client then connects to over WebSocket to actually talk. **POST** `/sessions/create` Create a voice Session: a short-lived, single-conversation reservation that lets a client hold a live voice chat with your agent. This is step 1 of 2. Creating the Session does not start any audio on its own; it returns a `ws_url` that a client then connects to over WebSocket to actually talk. Call this endpoint from your server with your API key, and return only the `ws_url` to your client. The API key must never reach the browser. The `ws_url` is the only thing the client needs, and it is safe to hand out because it is single-use and expires. For how to connect and talk, see "Connect the client and talk" below the request details. ```yaml operationId: createSession requestBody: required: true content: application/json: schema: type: object required: - agent_id - type properties: agent_id: type: integer description: ID of the agent the session talks to. example: 158910 type: type: string default: voice example: voice enum: - voice description: The session type. Only `voice` is supported. custom_variables: type: object additionalProperties: true description: | Per-session variables that personalize the conversation. Set server-side, so visitors cannot tamper with them. example: name: Demo User metadata: type: object additionalProperties: true description: | Key-value pairs stored on the session for your own tracking (e.g. CRM or lead IDs). Not shared with the agent; echoed back as `metadata` in the post-call webhook so you can correlate results with your records. example: crm_lead_id: lead_9876 source: website_form responses: '201': description: Session created. content: application/json: schema: type: object properties: session_id: type: integer description: Correlate this with call logs and support requests. example: 4521 token: type: string description: The Session Token. Single conversation, unguessable. example: sess_51gF2qw8LxNz0vY4mT7Ka3RjD9pBcE6HuWiQnZsX0oM expires_at: type: string format: date-time description: End of the 15-minute connect window (UTC). example: '2026-07-17T12:15:00Z' ws_url: type: string description: Connect your client to this URL as returned. example: >- wss://live.omnidim.io/chat/start_voice_chat?request_token=sess_51gF2qw8LxNz0vY4mT7Ka3RjD9pBcE6HuWiQnZsX0oM '400': description: | Unsupported session type (`unsupported_type`) or a malformed request body (`invalid_request`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: unsupported_type error_description: Only type 'voice' is supported. '401': description: Missing or invalid API key. '402': description: The organization balance is too low to start a call. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: insufficient_balance error_description: Balance is low. Please Choose Appropriate plan. '404': description: Agent not found in your organization. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: agent_not_found error_description: Agent not found or access denied '429': description: | The organization is at its concurrent call limit. Wait for a call to finish or purchase more concurrency, then retry. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: limit: type: integer description: Your organization's concurrent call limit. example: error: concurrency_limit_reached error_description: >- Your organization is at its concurrent call limit (2). Wait for a call to finish or purchase more concurrency. limit: 2 ``` **Browser SDK (connect after create)** ```js import { WebSession } from '@omnidim-ai/client'; // 1. Ask YOUR server to create the session (keeps the API key // server-side) and return the ws_url. const { ws_url } = await fetch('/create-omnidim-session', { method: 'POST', }).then((r) => r.json()); // 2. The browser connects to the ws_url and talks. No API key here. const session = new WebSession(); session.on('transcript', (t) => console.log(t.role, t.text)); await session.start({ wsUrl: ws_url }); ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/sessions/create" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` ## Connect the client and talk [#connect-the-client-and-talk] The `ws_url` alone does nothing until a client connects to it. Creating the Session is step 1; connecting a client is step 2. Pick one of the two client paths below. Both use the same `ws_url`. Your API key stays on your server. The browser (or any client) only ever receives the `ws_url`, which is single-use and expires, so it is safe to hand out. ### Token and URL rules [#token-and-url-rules] * The token is valid to connect for 15 minutes after creation. * It covers one conversation and dies when that conversation ends. * Connect to the `ws_url` exactly as returned. Never build, edit, or cache WebSocket URLs; the format is not part of the contract. * If the connection drops mid-call, reconnecting to the same `ws_url` re-attaches to the same conversation, so a brief network blip does not lose the call. ### Option A: browser, with the web SDK (recommended) [#option-a-browser-with-the-web-sdk-recommended] The [`@omnidim-ai/client` web SDK](/docs/sdks/web) turns the `ws_url` into a working conversation: microphone capture, agent audio playback, barge-in, and transcripts are all handled for you. ```bash npm install @omnidim-ai/client ``` ```js import { WebSession } from '@omnidim-ai/client'; // wsUrl came from POST /sessions/create on YOUR server. const session = new WebSession(); session.on('status', (s) => console.log('status', s)); // connecting | active | { state: 'ended', reason } session.on('transcript', (t) => console.log(t.role, t.text)); // { role: 'user' | 'agent', text, final } session.on('error', (e) => console.error(e)); await session.start({ wsUrl }); // asks for mic permission, then talks session.mute(true); // toggle the microphone session.stop(); // hang up ``` No bundler? Load the UMD build from a script tag and use the `OmnidimensionClient` global: ```html ``` Full guide, events, and ended reasons: [web SDK](/docs/sdks/web). ### Option B: any platform, raw WebSocket protocol [#option-b-any-platform-raw-websocket-protocol] For iOS, Android, Flutter, kiosks, or any client without the SDK, connect to the `ws_url` and speak the wire protocol directly: send 16 kHz mono PCM16 audio frames, and receive audio plus transcript events. ```js const ws = new WebSocket(wsUrl); // Send microphone audio, one JSON frame per chunk: ws.send(JSON.stringify({ type: 'audio', data: base64Pcm16 })); // Receive agent audio, transcripts, and control events: ws.onmessage = (e) => { const msg = JSON.parse(e.data); switch (msg.event) { case 'media': play(msg.media.payload); // base64 audio to play break; case 'clear': stopPlaybackNow(); // barge-in: flush playback immediately break; case 'end_call': hangUp(msg.media.payload.reason); break; // 'user' | 'partial_text' | 'system' | 'last_system_message' -> transcripts } }; // Hang up by closing the socket: ws.close(); ``` Full frame reference, event table, and close codes: [web call protocol](/docs/web-call-protocol). ### Recap [#recap] 1. Your server calls this endpoint and gets a `ws_url`. 2. Your server returns the `ws_url` to your client. 3. Your client connects to the `ws_url` (SDK or raw protocol) and the conversation runs until it hangs up or the socket closes. # Add user (/docs/api-reference/reseller/addUser) > Create a new child user and organization under the reseller. The new organization is linked to your reseller account automatically. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/users/add` Create a new child user and organization under the reseller. The new organization is linked to your reseller account automatically. ```yaml operationId: addUser requestBody: required: true content: application/json: schema: type: object required: - name - email - phone - password properties: name: type: string description: Full name of the new user. email: type: string format: email description: Email address. Also used as the login. phone: type: string description: Phone number including country code (e.g. `+15551234567`). example: '+15551234567' password: type: string format: password description: Account password for the new user. welcome_minutes_to_credit: type: integer description: Minutes to credit to the new account on signup. cost_per_min: type: number description: >- Cost per minute charged to this user (e.g. `0.20`). Must be at least the reseller's premium model rate. example: 0.2 concurrent_call_limit: type: integer description: Maximum number of concurrent calls allowed for this account. expiry_date: type: string format: date description: Account expiry date in `YYYY-MM-DD` format (e.g. `2026-12-31`). user_currency: type: string description: >- ISO 4217 currency code for the account (e.g. `USD`, `INR`). Defaults to the reseller's currency. example: USD example: name: Demo User email: demo@example.com phone: '+15551234567' password: SecurePass123! welcome_minutes_to_credit: 50 cost_per_min: 0.2 concurrent_call_limit: 2 expiry_date: '2026-12-31' user_currency: USD responses: '200': description: User created. content: application/json: schema: type: object properties: success: type: boolean message: type: string user_id: type: integer organization_id: type: integer organization_name: type: string organization_balance: type: number cost_per_min: type: number example: success: true message: User and organization created successfully user_id: 1234 organization_id: 9012 organization_name: Demo User's Organization organization_balance: 10 cost_per_min: 0.2 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/users/add" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Demo User", "email": "demo@example.com", "phone": "+15551234567", "password": "SecurePass123!", "welcome_minutes_to_credit": 50, "cost_per_min": 0.2, "concurrent_call_limit": 2, "expiry_date": "2026-12-31", "user_currency": "USD" }' ``` # Calculate credit operation (/docs/api-reference/reseller/calculateCreditOperation) > Preview the cost of a transfer or revert without moving any credits. Use this to confirm amounts before calling the transfer or revert endpoints. The response shape differs between forward transfers and reverts. See the examples. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/credits/calculate` Preview the cost of a transfer or revert without moving any credits. Use this to confirm amounts before calling the transfer or revert endpoints. The response shape differs between forward transfers and reverts. See the examples. ```yaml operationId: calculateCreditOperation requestBody: required: true content: application/json: schema: type: object required: - minutes properties: minutes: type: integer description: Number of minutes to calculate for. cost_per_min: type: number description: >- Rate per minute for a forward transfer (e.g. `0.20`). Not required when `is_revert` is `true`. example: 0.2 is_revert: type: boolean description: Set to `true` to calculate a revert instead of a forward transfer. default: false child_organization_id: type: integer description: >- ID of the child organization to revert credits from. Required when `is_revert` is `true`. example: user_id: 1234 region: IN name: Demo User email: demo@example.com phone: '+919876543210' responses: '200': description: Calculated outcome. content: application/json: schema: type: object properties: success: type: boolean my_cost: type: number description: Forward only. Amount deducted from the reseller balance. user_credit: type: number description: Forward only. Credit value the child receives at `cost_per_min`. profit: type: number description: Forward only. `user_credit - my_cost`. margin: type: number description: Forward only. Per-minute margin. reseller_balance: type: number description: Forward only. Current reseller balance before the transfer. reseller_available_minutes: type: integer description: Forward only. Minutes the reseller could still transfer at its own rate. new_reseller_balance: type: number description: Forward only. Reseller balance after the proposed transfer. refund_amount: type: number description: Revert only. Credits added back to the reseller. deduction_amount: type: number description: Revert only. Credits removed from the child at its current rate. child_balance: type: number description: Revert only. Child balance before the revert. child_available_minutes: type: integer description: Revert only. Minutes available in the child's balance. new_child_balance: type: number description: Revert only. Child balance after the proposed revert. reseller_rate: type: number description: Reseller's wholesale rate (cost per minute). currency_symbol: type: string examples: forward: summary: Forward transfer (500 min at $0.20/min) value: success: true my_cost: 45 user_credit: 50 profit: 5 margin: 0.01 reseller_rate: 0.09 reseller_balance: 66.113 reseller_available_minutes: 734 new_reseller_balance: 21.113 currency_symbol: $ revert: summary: Revert (10 min from a child) value: success: true refund_amount: 0.9 deduction_amount: 2 reseller_rate: 0.09 child_balance: 10 child_available_minutes: 50 new_child_balance: 8 currency_symbol: $ ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/credits/calculate" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": 1234, "region": "IN", "name": "Demo User", "email": "demo@example.com", "phone": "+919876543210" }' ``` # Credit transfer logs (/docs/api-reference/reseller/getResellerCreditLogs) > Paginated history of all credit transfers and reverts for the reseller account. Returns reverse-chronological order by default. Date filters are inclusive. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **GET** `/reseller/credits/logs` Paginated history of all credit transfers and reverts for the reseller account. Returns reverse-chronological order by default. Date filters are inclusive. ```yaml operationId: getResellerCreditLogs parameters: - name: page in: query description: Page number for pagination. schema: type: integer default: 1 - name: page_size in: query description: Number of records per page (max 100). schema: type: integer default: 20 - name: date_from in: query description: Filter logs from this date in `YYYY-MM-DD` format (e.g. `2026-01-01`). schema: type: string format: date - name: date_to in: query description: Filter logs up to and including this date in `YYYY-MM-DD` format (e.g. `2026-03-31`). schema: type: string format: date responses: '200': description: Credit logs. content: application/json: schema: type: object properties: success: type: boolean data: type: object properties: logs: type: array items: type: object properties: id: type: integer transfer_reference: type: string reseller_organization: type: object properties: id: type: integer name: type: string from_organization: type: object properties: id: type: integer name: type: string to_organization: type: object properties: id: type: integer name: type: string credit_amount: type: number cost_amount: type: number from_balance_before: type: number from_balance_after: type: number to_balance_before: type: number to_balance_after: type: number transfer_date: type: string format: date-time performed_by: type: object properties: id: type: integer name: type: string email: type: string format: email status: type: string transaction_type: type: string description: One of `credit_transfer`, `credit_revert`, or `channel_allocation`. channels_count: type: integer previous_channels: type: integer new_channels: type: integer notes: type: string total_records: type: integer page: type: integer page_size: type: integer total_pages: type: integer example: success: true data: logs: - id: 4 transfer_reference: CT-20260115110440-5678-4002 reseller_organization: id: 5678 name: Demo Reseller from_organization: id: 5678 name: Demo Reseller to_organization: id: 4002 name: Beta Co credit_amount: 4 cost_amount: -1.8 from_balance_before: 61.613 from_balance_after: 59.813 to_balance_before: 10 to_balance_after: 14 transfer_date: '2026-01-15T16:34:40+05:30' performed_by: id: 1000 name: Demo Reseller Admin email: admin@example.com status: success transaction_type: credit_transfer channels_count: 0 previous_channels: 0 new_channels: 0 notes: Transfer of 20 minutes at 0.20/min - id: 3 transfer_reference: CH-20260115110440-5678-4002 reseller_organization: id: 5678 name: Demo Reseller from_organization: id: 5678 name: Demo Reseller to_organization: id: 4002 name: Beta Co credit_amount: 0 cost_amount: 0 from_balance_before: 0 from_balance_after: 0 to_balance_before: 0 to_balance_after: 0 transfer_date: '2026-01-15T16:34:40+05:30' performed_by: id: 1000 name: Demo Reseller Admin email: admin@example.com status: success transaction_type: channel_allocation channels_count: 1 previous_channels: 4 new_channels: 5 notes: 'Concurrency limit changed from 4 to 5 (Delta: 1)' total_records: 2 page: 1 page_size: 20 total_pages: 1 ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/reseller/credits/logs" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Get KYC requirements for a region (/docs/api-reference/reseller/getResellerKycRequirements) > Get the ordered list of verification steps for a region and the fields each step needs, so your integration can build a verification form without hardcoding the sequence. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **GET** `/reseller/kyc/requirements` Get the ordered list of verification steps for a region and the fields each step needs, so your integration can build a verification form without hardcoding the sequence. ```yaml operationId: getResellerKycRequirements parameters: - name: region in: query required: true description: Region to get verification requirements for. schema: type: string enum: - IN - US example: IN responses: '200': description: Verification requirements for the region. content: application/json: schema: type: object properties: success: type: boolean region: type: string enum: - IN - US steps: type: array items: type: object properties: step: type: string enum: - register - verify-otp - resend-otp - verify-pan - aadhaar-otp - aadhaar-verify - verify-gst - skip-gst - preview - accept required: type: array items: type: string description: Body fields this step requires, beyond `user_id` and `region`. cooldown: type: boolean description: >- Whether this step is rate-limited. When true, wait about 30 seconds between attempts. example: success: true region: IN steps: - step: register required: - email - name - phone cooldown: false - step: verify-otp required: - email_otp - mobile_otp cooldown: false - step: resend-otp required: [] cooldown: false - step: verify-pan required: - business_type - pan cooldown: false - step: aadhaar-otp required: - aadhaar cooldown: true - step: aadhaar-verify required: - otp cooldown: true - step: verify-gst required: - gst cooldown: false - step: skip-gst required: [] cooldown: false - step: preview required: [] cooldown: false - step: accept required: [] cooldown: false '401': description: Missing or invalid API key. '403': description: The caller's account cannot use the reseller API. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: forbidden error_description: Access denied. Only reseller accounts can use this endpoint. '404': description: No verification flow is configured for this region. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: not_available error_description: Verification is not available for this region. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/reseller/kyc/requirements?region=IN" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` Step-by-step verification exists for regions that require it. India is the one today, and the check is an Aadhaar eKYC. A region that returns an empty `steps` list has no step-based verification. Check `kyc_required` on [KYC status](/docs/api-reference/reseller/getResellerKycStatus) to know whether that region needs verification at all before a purchase. The full loop is in the [Reseller API guide](/docs/reseller-api). # Get KYC status (/docs/api-reference/reseller/getResellerKycStatus) > Get the identity verification status of a client for every region where verification is required. Use next_step to know which step to call next, so you drive the whole flow off one poll instead of hardcoding the sequence. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **GET** `/reseller/kyc/status` Get the identity verification status of a client for every region where verification is required. Use `next_step` to know which step to call next, so you drive the whole flow off one poll instead of hardcoding the sequence. ```yaml operationId: getResellerKycStatus parameters: - name: user_id in: query required: true description: ID of the child user to check. schema: type: integer example: 1234 responses: '200': description: KYC status per region. content: application/json: schema: type: object properties: success: type: boolean user_id: type: integer regions: type: array description: One entry per region, each with that region's verification state. items: type: object properties: region: type: string enum: - IN - US kyc_required: type: boolean description: >- Whether this client must complete verification before buying a number in this region. can_purchase: type: boolean description: >- Whether this client can buy a number in this region right now. Always agrees with what a purchase attempt would allow. status: type: string description: | Where this client has reached in verification. `not_started` before anything is submitted, `completed` once they are verified. Drive your integration off `next_step`, not this value. next_step: type: string nullable: true description: The step to call next. `null` once `status` is `completed`. enum: - register - verify-otp - verify-pan - aadhaar-otp - aadhaar-verify - verify-gst - preview - accept example: success: true user_id: 1234 regions: - region: IN kyc_required: true can_purchase: false status: pan_verified next_step: aadhaar-otp '401': description: Missing or invalid API key. '403': description: | A `user_id` was sent by a key that is not a reseller admin, or the named client's account is currently unavailable. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: forbidden error_description: Access denied. Only reseller accounts can use this endpoint. '404': description: Child user not found under this reseller account. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: not_found error_description: Child user not found. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/reseller/kyc/status?user_id=1234" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` `next_step` is the field to drive your integration from: read it once here to get the first step, then chain from each step's response until the status is `completed`. `can_purchase` is the exact gate a purchase enforces, so this answer and a purchase attempt can never disagree. The full loop, with the step table, is in the [Reseller API guide](/docs/reseller-api). # List child organizations (/docs/api-reference/reseller/listChildOrganizations) > List all child organizations and their users under the reseller account. Returns each organization's balance, cost-per-minute rate, and concurrency limit, plus the dashboard menu access flags scoped to your reseller's permissions for every user. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **GET** `/reseller/organizations` List all child organizations and their users under the reseller account. Returns each organization's balance, cost-per-minute rate, and concurrency limit, plus the dashboard menu access flags scoped to your reseller's permissions for every user. ```yaml operationId: listChildOrganizations responses: '200': description: List of organizations. content: application/json: schema: type: object properties: success: type: boolean reseller_organization: type: object properties: id: type: integer name: type: string concurrent_call_limit: type: integer organizations: type: array items: type: object properties: id: type: integer name: type: string cost_per_min: type: number org_balance: type: number reseller_org_balance_currency_symbol: type: string concurrent_call_limit: type: integer org_user_currency_symbol: type: string timezone: type: string created_date: type: string format: date-time total_users: type: integer users: type: array items: type: object properties: id: type: integer name: type: string email: type: string format: email phone: type: string login_date: type: string format: date-time nullable: true is_active: type: boolean expiry_date: type: string format: date nullable: true access_level: type: boolean dashboard_menu_access: type: object description: | Reseller-managed dashboard menu access flags. Each property is a boolean toggle for a feature area in the child user's dashboard. On read endpoints, only flags the reseller themselves has enabled are returned (so a child cannot have a flag the reseller doesn't have). properties: is_bots_menu_access: type: boolean is_leads_access: type: boolean is_voice_cloning_access: type: boolean is_workflow_access: type: boolean is_asr_evaluation_menu_access: type: boolean is_train_with_call_recording_menu_access: type: boolean is_call_logs_menu_access: type: boolean is_call_simulation_menu_access: type: boolean is_omni_crm_access: type: boolean access_to_monitor_live_call: type: boolean is_whatsapp_flow_enabled: type: boolean is_billing_menu_access: type: boolean is_knowledge_base_access: type: boolean is_integration_access: type: boolean is_phone_number_access: type: boolean is_bulk_call_access: type: boolean is_analytics_access: type: boolean total_organizations: type: integer total_users_across_orgs: type: integer reseller_access_control: type: object properties: dashboard_menu_access: type: object description: | Reseller-managed dashboard menu access flags. Each property is a boolean toggle for a feature area in the child user's dashboard. On read endpoints, only flags the reseller themselves has enabled are returned (so a child cannot have a flag the reseller doesn't have). properties: is_bots_menu_access: type: boolean is_leads_access: type: boolean is_voice_cloning_access: type: boolean is_workflow_access: type: boolean is_asr_evaluation_menu_access: type: boolean is_train_with_call_recording_menu_access: type: boolean is_call_logs_menu_access: type: boolean is_call_simulation_menu_access: type: boolean is_omni_crm_access: type: boolean access_to_monitor_live_call: type: boolean is_whatsapp_flow_enabled: type: boolean is_billing_menu_access: type: boolean is_knowledge_base_access: type: boolean is_integration_access: type: boolean is_phone_number_access: type: boolean is_bulk_call_access: type: boolean is_analytics_access: type: boolean example: success: true reseller_organization: id: 5678 name: Demo Reseller concurrent_call_limit: 10 organizations: - id: 4001 name: Acme Inc cost_per_min: 0.09 org_balance: 24442.6 reseller_org_balance_currency_symbol: $ concurrent_call_limit: 1 org_user_currency_symbol: $ timezone: US/Pacific created_date: '2025-10-09T10:20:58.220633' total_users: 1 users: - id: 1110 name: Demo User email: demo@example.com phone: false login_date: '2026-04-23T11:00:06.685330' is_active: true expiry_date: null access_level: false dashboard_menu_access: is_bots_menu_access: true is_leads_access: false is_voice_cloning_access: false is_workflow_access: false is_asr_evaluation_menu_access: false is_train_with_call_recording_menu_access: false is_call_logs_menu_access: true is_call_simulation_menu_access: false access_to_monitor_live_call: false is_whatsapp_flow_enabled: true is_billing_menu_access: true is_knowledge_base_access: true is_integration_access: true is_phone_number_access: true is_bulk_call_access: true is_analytics_access: true - id: 4002 name: Beta Co cost_per_min: 0.2 org_balance: 13 reseller_org_balance_currency_symbol: $ concurrent_call_limit: 5 org_user_currency_symbol: $ timezone: US/Pacific created_date: '2026-05-11T11:04:09.426809' total_users: 1 users: - id: 1234 name: Demo User 2 email: demo2@example.com phone: '+15551234567' login_date: null is_active: true expiry_date: null access_level: false dashboard_menu_access: is_bots_menu_access: true is_leads_access: false is_voice_cloning_access: true is_workflow_access: false is_asr_evaluation_menu_access: false is_train_with_call_recording_menu_access: false is_call_logs_menu_access: true is_call_simulation_menu_access: false access_to_monitor_live_call: false is_whatsapp_flow_enabled: true is_billing_menu_access: true is_knowledge_base_access: true is_integration_access: true is_phone_number_access: false is_bulk_call_access: true is_analytics_access: true total_organizations: 2 total_users_across_orgs: 2 reseller_access_control: dashboard_menu_access: is_bots_menu_access: true is_leads_access: true is_voice_cloning_access: true is_workflow_access: true is_asr_evaluation_menu_access: true is_train_with_call_recording_menu_access: true is_call_logs_menu_access: true is_call_simulation_menu_access: true access_to_monitor_live_call: true is_whatsapp_flow_enabled: true is_billing_menu_access: true is_knowledge_base_access: true is_integration_access: true is_phone_number_access: true is_bulk_call_access: true is_analytics_access: true ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/reseller/organizations" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Revert credits (/docs/api-reference/reseller/revertCreditsFromChild) > Take unused minutes back from a client. The exact mirror of a transfer, so you do not pass a rate. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/credits/revert` Take unused minutes back from a client. The exact mirror of a transfer, so you do not pass a rate. The client is deducted at their own current rate, which is what they were charged, and you are refunded at your rate, which is what it cost you. Use the calculate endpoint first to preview both amounts. ```yaml operationId: revertCreditsFromChild requestBody: required: true content: application/json: schema: type: object required: - from_organization_id - minutes properties: from_organization_id: type: integer description: ID of the child organization to revert credits from. minutes: type: integer description: Number of minutes to revert. example: from_organization_id: 4002 minutes: 5 responses: '200': description: Credits reverted. content: application/json: schema: type: object properties: success: type: boolean message: type: string new_from_balance: type: number description: Child balance after the revert. new_to_balance: type: number description: Reseller balance after the revert. example: success: true message: Credits reverted successfully new_from_balance: 13 new_to_balance: 60.263 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/credits/revert" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from_organization_id": 4002, "minutes": 5 }' ``` # Set child concurrency limit (/docs/api-reference/reseller/setChildConcurrency) > Set the maximum number of simultaneous calls a client can run. new_limit is the absolute figure you want, not a change to the current one. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/concurrency` Set the maximum number of simultaneous calls a client can run. `new_limit` is the absolute figure you want, not a change to the current one. Assigning is free: any figure is accepted, nothing is deducted from you, and this call never fails for lack of capacity. It does not create capacity, though. Your own concurrent call limit is the ceiling for your whole account family at dial time, so what you set here is a per-client cap, and your own limit is the capacity those caps compete for. Assign a client more than you hold and the extra simply cannot be dialled. ```yaml operationId: setChildConcurrency requestBody: required: true content: application/json: schema: type: object required: - child_organization_id - new_limit properties: child_organization_id: type: integer description: ID of the child organization to update. new_limit: type: integer description: The desired absolute concurrent call limit (must be `>= 0`). example: child_organization_id: 4002 new_limit: 5 responses: '200': description: Concurrency limit updated. content: application/json: schema: type: object properties: success: type: boolean message: type: string child_limit: type: integer description: The child organization's new concurrent call limit. reseller_available: type: integer description: | Your own concurrent call limit, which is the shared dial-time ceiling for your whole family. Assigning to clients does not draw it down. example: success: true message: Concurrency limit updated successfully. child_limit: 5 reseller_available: 7 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/concurrency" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_organization_id": 4002, "new_limit": 5 }' ``` # Update user access control (/docs/api-reference/reseller/setUserAccessControl) > Enable or disable dashboard menu access flags for a child user. Only the flags you pass are changed. Flags outside your reseller's permissions are silently ignored. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/users/access-control` Enable or disable dashboard menu access flags for a child user. Only the flags you pass are changed. Flags outside your reseller's permissions are silently ignored. ```yaml operationId: setUserAccessControl requestBody: required: true content: application/json: schema: type: object required: - user_id - dashboard_menu_access properties: user_id: type: integer description: ID of the child user to update. dashboard_menu_access: allOf: - type: object description: | Reseller-managed dashboard menu access flags. Each property is a boolean toggle for a feature area in the child user's dashboard. On read endpoints, only flags the reseller themselves has enabled are returned (so a child cannot have a flag the reseller doesn't have). properties: is_bots_menu_access: type: boolean is_leads_access: type: boolean is_voice_cloning_access: type: boolean is_workflow_access: type: boolean is_asr_evaluation_menu_access: type: boolean is_train_with_call_recording_menu_access: type: boolean is_call_logs_menu_access: type: boolean is_call_simulation_menu_access: type: boolean is_omni_crm_access: type: boolean access_to_monitor_live_call: type: boolean is_whatsapp_flow_enabled: type: boolean is_billing_menu_access: type: boolean is_knowledge_base_access: type: boolean is_integration_access: type: boolean is_phone_number_access: type: boolean is_bulk_call_access: type: boolean is_analytics_access: type: boolean description: | Flags to update. Only pass the flags you want to change. Others are left untouched. Flags outside your reseller's permissions are silently dropped. example: user_id: 1234 dashboard_menu_access: is_bots_menu_access: true is_call_logs_menu_access: true is_phone_number_access: false responses: '200': description: Access control updated. content: application/json: schema: type: object properties: success: type: boolean message: type: string updated_flags: allOf: - type: object description: | Reseller-managed dashboard menu access flags. Each property is a boolean toggle for a feature area in the child user's dashboard. On read endpoints, only flags the reseller themselves has enabled are returned (so a child cannot have a flag the reseller doesn't have). properties: is_bots_menu_access: type: boolean is_leads_access: type: boolean is_voice_cloning_access: type: boolean is_workflow_access: type: boolean is_asr_evaluation_menu_access: type: boolean is_train_with_call_recording_menu_access: type: boolean is_call_logs_menu_access: type: boolean is_call_simulation_menu_access: type: boolean is_omni_crm_access: type: boolean access_to_monitor_live_call: type: boolean is_whatsapp_flow_enabled: type: boolean is_billing_menu_access: type: boolean is_knowledge_base_access: type: boolean is_integration_access: type: boolean is_phone_number_access: type: boolean is_bulk_call_access: type: boolean is_analytics_access: type: boolean description: The flags that were actually applied (after the reseller-scope filter). example: success: true message: User access control updated successfully updated_flags: is_bots_menu_access: true is_call_logs_menu_access: true is_phone_number_access: false ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/users/access-control" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": 1234, "dashboard_menu_access": { "is_bots_menu_access": true, "is_call_logs_menu_access": true, "is_phone_number_access": false } }' ``` # Update user expiry (/docs/api-reference/reseller/setUserExpiry) > Set or remove the expiry date on a child user. The user must belong to a child organization of your reseller. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/users/expiry` Set or remove the expiry date on a child user. The user must belong to a child organization of your reseller. ```yaml operationId: setUserExpiry requestBody: required: true content: application/json: schema: type: object required: - user_id properties: user_id: type: integer description: ID of the child user to update. expiry_date: type: string format: date nullable: true description: Expiry date in `YYYY-MM-DD` format. Omit or pass `null` to remove the expiry. example: user_id: 1234 expiry_date: '2026-12-31' responses: '200': description: Expiry updated. content: application/json: schema: type: object properties: success: type: boolean message: type: string user_id: type: integer expiry_date: type: string format: date nullable: true example: success: true message: User expiry date updated successfully user_id: 1234 expiry_date: '2026-12-31' ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/users/expiry" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": 1234, "expiry_date": "2026-12-31" }' ``` # Submit a KYC verification step (/docs/api-reference/reseller/submitResellerKycStep) > Run one step of a client's identity verification. One endpoint handles every step: the step path parameter names the step, and the body carries user_id, region, and whatever that step needs. Pick a step from the examples below to see its body. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/kyc/steps/{step}` Run one step of a client's identity verification. One endpoint handles every step: the `step` path parameter names the step, and the body carries `user_id`, `region`, and whatever that step needs. Pick a step from the examples below to see its body. ```yaml operationId: submitResellerKycStep parameters: - name: step in: path required: true description: The verification step to run. schema: type: string enum: - register - verify-otp - resend-otp - verify-pan - aadhaar-otp - aadhaar-verify - verify-gst - skip-gst - preview - accept requestBody: required: true content: application/json: schema: type: object required: - user_id - region properties: user_id: type: integer description: ID of the client completing verification. region: type: string enum: - IN - US description: Region this verification is for. name: type: string description: Customer's full name. Required for `register`. example: Demo User email: type: string format: email description: Customer's email address. Required for `register`. example: demo@example.com phone: type: string description: Customer's phone number including country code. Required for `register`. example: '+919876543210' mobile_otp: type: string description: OTP the client received by mobile. Required for `verify-otp`. email_otp: type: string description: OTP the client received by email. Required for `verify-otp`. pan: type: string description: Customer's PAN. Required for `verify-pan`. business_type: type: string enum: - proprietorship - company - firm description: Customer's business type. Required for `verify-pan`. aadhaar: type: string description: | Customer's Aadhaar number. Required for `aadhaar-otp`. Rate limited to one attempt roughly every 30 seconds. otp: type: string description: | OTP the client received for Aadhaar verification. Required for `aadhaar-verify`. Rate limited to one attempt roughly every 30 seconds. gst: type: string description: Customer's GST number. Required for `verify-gst`. example: user_id: 1234 region: IN name: Demo User email: demo@example.com phone: '+919876543210' responses: '200': description: | Step completed. `preview` is only present in the response to the `preview` step, and only carries the fields listed below; any field the verification provider does not have yet is omitted. content: application/json: schema: type: object properties: success: type: boolean status: type: string description: | Where this client has reached in verification. `not_started` before anything is submitted, `completed` once they are verified. Drive your integration off `next_step`, not this value. next_step: type: string nullable: true description: >- The step to run next. Chain to it without re-reading status. `null` once verification is complete. enum: - register - verify-otp - verify-pan - aadhaar-otp - aadhaar-verify - verify-gst - preview - accept message: type: string description: Human-readable confirmation for the step that just ran. preview: type: object description: Present only in the response to the `preview` step. properties: client: type: object properties: name: type: string email: type: string format: email mobile: type: string country_code: type: string pan: type: object properties: pan: type: string business_type: type: string aadhar: type: object properties: name: type: string address: type: string gst: type: object properties: gst_num: type: string gstin: type: string examples: accept: summary: accept value: success: true status: completed message: Verification completed. preview: summary: preview value: success: true status: preview message: Review the details, then call accept. preview: client: name: Demo User email: demo@example.com mobile: '9876543210' country_code: '91' pan: pan: ABCDE1234F business_type: proprietorship aadhar: name: Demo User address: 123 Example Street, Example City, 560001 gst: gst_num: 29ABCDE1234F1Z5 gstin: 29ABCDE1234F1Z5 '400': description: The request body is missing a field the step requires. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: invalid_request error_description: 'Missing required fields: pan, business_type.' '401': description: Missing or invalid API key. '403': description: | A `user_id` was sent by a key that is not a reseller admin, or the named client's account is currently unavailable. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: forbidden error_description: Access denied. Only reseller accounts can use this endpoint. '404': description: | The child user was not found (`not_found`), or verification is not available for this region or step (`not_available`). content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: not_found error_description: Child user not found. '409': description: This step was called before its prerequisite step was completed. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: step_order error_description: Complete the contact verification (OTP) step first. '422': description: The verification provider rejected the submitted details. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: verification_failed error_description: OTP verification failed. Please try again. '429': description: An Aadhaar step was retried before the cooldown elapsed. content: application/json: schema: type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. example: error: too_fast error_description: Please wait a moment before retrying this step. '500': description: Unexpected server error. content: application/json: schema: allOf: - type: object properties: error: type: string description: Machine-readable error code. error_description: type: string description: Human-readable explanation. - type: object properties: ref: type: string description: Reference to quote to support. example: error: server_error error_description: >- Something went wrong on our side. Please try again shortly, or contact support with reference a1b2c3d4e5f6. ref: a1b2c3d4e5f6 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/kyc/steps/{step}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": 1234, "region": "IN", "name": "Demo User", "email": "demo@example.com", "phone": "+919876543210" }' ``` ## What each step sends and returns [#what-each-step-sends-and-returns] Every call is a `POST` to this endpoint with the step name in the path. The body always carries `user_id` and `region`, plus the fields in the table below. Steps with no fields of their own take just those two. Every response has the same shape: ```json { "success": true, "status": "otp_verified", "next_step": "verify-pan", "message": "Contact details verified." } ``` `next_step` is the field to act on. Run it, read the next one off that response, and keep going until the status comes back `completed`. | Step | Body fields you add | `next_step` you get back | `message` | | ---------------- | ------------------------- | ------------------------ | ------------------------------------------------------- | | `register` | `name`, `email`, `phone` | `verify-otp` | Verification codes sent to the email and mobile number. | | `verify-otp` | `mobile_otp`, `email_otp` | `verify-pan` | Contact details verified. | | `verify-pan` | `pan`, `business_type` | `aadhaar-otp` | PAN verified. | | `aadhaar-otp` | `aadhaar` | `aadhaar-verify` | Aadhaar verification code sent. | | `aadhaar-verify` | `otp` | `verify-gst` | Aadhaar verified. | | `verify-gst` | `gst` | `preview` | GST verified. | | `skip-gst` | none | `preview` | GST step skipped. | | `preview` | none | `accept` | Review the details, then call accept. | | `accept` | none | `null` | Verification completed. | `verify-gst` and `skip-gst` are a choice: run one or the other, both land on `preview`. Once the status is `completed` the client is verified and can buy a number. The email code arrives in the client's inbox; the mobile code arrives as an automated voice call that reads the code out. ### The one step that returns more [#the-one-step-that-returns-more] `preview` returns everything the client should confirm before `accept` commits it, alongside the usual fields: ```json { "success": true, "status": "preview", "next_step": "accept", "message": "Review the details, then call accept.", "preview": { "client": { "name": "Demo User", "email": "demo@example.com", "mobile": "9876543210", "country_code": "91" }, "pan": { "pan": "ABCDE1234F", "business_type": "proprietorship" }, "aadhar": { "name": "Demo User", "address": "..." }, "gst": { "gst_num": "29ABCDE1234F1Z5", "gstin": "..." } } } ``` Anything the verification provider does not hold yet is left out, so treat each block as optional. ### Resending a code [#resending-a-code] `resend-otp` takes `user_id` and `region`, and answers `next_step: verify-otp` like the step it belongs to. It sits outside the sequence and is never returned as a `next_step`, so call it only when the client says a code did not arrive. One optional field controls where the resend goes: `type` is `email`, `mobile`, or `both` (default `both`). Separate per-channel resend buttons in your interface map to `type: "email"` and `type: "mobile"`; the mobile resend places the voice call again. Roughly one attempt every 30 seconds on `aadhaar-otp` and `aadhaar-verify`. Repeated rapid retries can lock the client out at the identity authority, which is not reversible from this API. Calling too soon returns `429`. ### Order is enforced [#order-is-enforced] Calling a step before its prerequisite is complete returns `409` naming the step to run first, so following `next_step` is all you need to stay in order. ### What you may keep [#what-you-may-keep] You are relaying what the client types into your own interface, nothing more. Never store the values you send in these requests once the call completes, and redact them from your own request logs. That covers the OTPs, the PAN, the Aadhaar number, and the GST number. The full loop, with the step diagram, is in the [Reseller API guide](/docs/reseller-api). # Transfer credits to a child (/docs/api-reference/reseller/transferCreditsToChild) > Transfer minutes from your balance to a client, at the rate you set. > **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api\&lock=1). **POST** `/reseller/credits/transfer` Transfer minutes from your balance to a client, at the rate you set. Two different amounts move: your balance is debited at your own rate, and the client is credited at `cost_per_min`. The gap between them is your margin, so a `cost_per_min` below your own rate is refused as a loss rather than silently costing you money on every call. The target organization must be a direct child of your reseller. Use the calculate endpoint first to preview both amounts. ```yaml operationId: transferCreditsToChild requestBody: required: true content: application/json: schema: type: object required: - to_organization_id - minutes - cost_per_min properties: to_organization_id: type: integer description: ID of the child organization to transfer credits to. minutes: type: integer description: Number of minutes to transfer. cost_per_min: type: number description: Rate per minute to charge the child organization (e.g. `0.20`). example: 0.2 example: to_organization_id: 4002 minutes: 20 cost_per_min: 0.2 responses: '200': description: Transfer succeeded. content: application/json: schema: type: object properties: success: type: boolean message: type: string new_from_balance: type: number description: Reseller balance after the transfer. new_to_balance: type: number description: Child balance after the transfer. example: success: true message: Credits allocated successfully new_from_balance: 59.813 new_to_balance: 14 ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/reseller/credits/transfer" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to_organization_id": 4002, "minutes": 20, "cost_per_min": 0.2 }' ``` # Create simulation (/docs/api-reference/simulation/createSimulation) > Create a new test simulation with scenarios. **POST** `/simulations` Create a new test simulation with scenarios. ```yaml operationId: createSimulation requestBody: required: true content: application/json: schema: type: object required: - name - agent_id properties: name: type: string description: Name of the simulation. example: My Simulation agent_id: type: integer description: ID of the agent to test. example: 158910 number_of_call_to_make: type: integer default: 1 minimum: 1 maximum: 3 description: Number of calls to make per scenario (default 1, max 3). concurrent_call_count: type: integer default: 3 minimum: 1 maximum: 3 description: Number of concurrent calls to run (default 3, max 3). max_call_duration_in_minutes: type: integer default: 3 minimum: 1 maximum: 10 description: Maximum duration for each call in minutes (default 3, max 10). scenarios: type: array description: List of test scenarios to execute. items: type: object required: - name - description - expected_result properties: name: type: string description: Name of the test scenario. example: Polite cancellation description: type: string description: Detailed instructions for the test scenario. example: Ask to cancel a subscription, but be friendly. expected_result: type: string description: Expected outcome or behavior from the agent. example: Agent acknowledges the request and routes to retention. selected_voices: type: array description: >- Voice configurations for the test calls. If multiple voices are selected, the agent randomly picks one per call per scenario. items: type: object required: - id - provider properties: id: type: string description: Voice ID from the provider. provider: type: string enum: - eleven_labs - play_ht - deepgram - cartesia - rime example: eleven_labs responses: '200': description: Simulation created. content: application/json: schema: type: object properties: success: type: boolean simulation: type: object description: A test simulation for an agent. properties: id: type: integer example: 772 name: type: string bot_id: type: object description: The agent under test. properties: id: type: integer name: type: string status: type: string enum: - Draft - Pending - In Progress - Calculating Summary - Completed - Stopped number_of_call_to_make: type: integer concurrent_call_count: type: integer max_call_duration_in_minutes: type: integer scenarios_ids: type: array description: Scenario records attached to this simulation. items: type: object additionalProperties: true what_went_wrong: oneOf: - type: string - type: boolean suggestions_for_improvement: oneOf: - type: string - type: boolean prompt_suggestion: oneOf: - type: string - type: boolean is_auto_prompt_suggestions_applied: type: boolean how_many_scenario_to_generate: type: integer summary: oneOf: - type: string - type: boolean analyticsData: type: object properties: Positive: type: integer Negative: type: integer Neutral: type: integer total_simulation_remaining_records: type: integer total_simulation_in_progress_records: type: integer total_simulation_finished_records: type: integer total_records: type: integer progress: type: array minItems: 1 maxItems: 1 items: type: integer description: >- Single-element array containing the progress percentage (0-100). Quirky shape preserved for backwards compatibility. example: - 0 simulation_call_recording: type: array items: type: object additionalProperties: true can_stop: type: boolean active_calls_count: type: integer create_date: type: string example: 05/06/2026 13:31:54 example: success: true simulation: id: 456 name: Customer Support Test bot_id: id: 1234 name: Customer Support Agent scenarios_ids: [] status: Draft number_of_call_to_make: 1 concurrent_call_count: 3 max_call_duration_in_minutes: 3 what_went_wrong: false suggestions_for_improvement: false prompt_suggestion: false is_auto_prompt_suggestions_applied: false how_many_scenario_to_generate: 0 summary: false analyticsData: Positive: 0 Negative: 0 Neutral: 0 total_simulation_remaining_records: 0 total_simulation_in_progress_records: 0 total_simulation_finished_records: 0 total_records: 0 progress: - 0 create_date: 05/08/2026 09:10:29 simulation_call_recording: [] can_stop: false active_calls_count: 0 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Create a basic simulation response = client.simulation.create( name="Restaurant Order Taking Test", agent_id=123, number_of_call_to_make=1, concurrent_call_count=3, max_call_duration_in_minutes=3, scenarios=[ { "name": "Order Pizza", "description": "1. Act as a customer wanting to order pizza\n2. Ask for menu items\n3. Order a large pepperoni pizza\n4. Provide contact details\n5. End call with thank you", "expected_result": "Order should be placed successfully and confirmation provided", "selected_voices": [ {"id": "voice_id_1", "provider": "eleven_labs"}, {"id": "voice_id_2", "provider": "play_ht"} ] } ] ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/simulations" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Delete simulation (/docs/api-reference/simulation/deleteSimulation) > Permanently delete a simulation. **DELETE** `/simulations/{simulation_id}` Permanently delete a simulation. ```yaml operationId: deleteSimulation responses: '200': description: Simulation deleted. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: success: true message: Simulation deleted successfully ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Delete a simulation simulation_id = 456 response = client.simulation.delete(simulation_id) print(response) ``` **curl** ```bash curl -X DELETE "https://omnidim.io/api/v1/simulations/{simulation_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Enhance prompt (/docs/api-reference/simulation/enhancePrompt) > Generate prompt-improvement suggestions for a completed simulation. **POST** `/simulations/{simulation_id}/enhance-prompt` Generate prompt-improvement suggestions for a completed simulation. ```yaml operationId: enhancePrompt parameters: - name: simulation_id in: path required: true schema: type: integer responses: '200': description: | Enhanced prompt suggestions. The simulation must be in `Completed` status; otherwise the API returns 400. content: application/json: schema: type: object properties: success: type: boolean previous_context: type: array description: The agent's existing context-breakdown sections. items: type: object properties: title: type: string prompt: type: string prompt_breakdown: type: array description: Suggested replacement sections, structured the same way. items: type: object properties: title: type: string prompt: type: string example: success: true previous_context: - title: Purpose prompt: This agent helps customers with product inquiries and support issues. prompt_breakdown: - title: Purpose prompt: >- This agent helps customers with product inquiries, support issues, and frequently asked product questions. '400': description: > Simulation is not in `Completed` status. Body: `{"error": "invalid_state", "error_description": "Enhanced prompt suggestions are only available for completed simulations"}`. ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get enhanced prompt suggestions simulation_id = 456 response = client.simulation.enhance_prompt(simulation_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/simulations/{simulation_id}/enhance-prompt" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Get simulation (/docs/api-reference/simulation/getSimulation) > Detailed simulation information. **GET** `/simulations/{simulation_id}` Detailed simulation information. ```yaml operationId: getSimulation responses: '200': description: Simulation details. content: application/json: schema: type: object properties: success: type: boolean simulation: type: object description: A test simulation for an agent. properties: id: type: integer example: 772 name: type: string bot_id: type: object description: The agent under test. properties: id: type: integer name: type: string status: type: string enum: - Draft - Pending - In Progress - Calculating Summary - Completed - Stopped number_of_call_to_make: type: integer concurrent_call_count: type: integer max_call_duration_in_minutes: type: integer scenarios_ids: type: array description: Scenario records attached to this simulation. items: type: object additionalProperties: true what_went_wrong: oneOf: - type: string - type: boolean suggestions_for_improvement: oneOf: - type: string - type: boolean prompt_suggestion: oneOf: - type: string - type: boolean is_auto_prompt_suggestions_applied: type: boolean how_many_scenario_to_generate: type: integer summary: oneOf: - type: string - type: boolean analyticsData: type: object properties: Positive: type: integer Negative: type: integer Neutral: type: integer total_simulation_remaining_records: type: integer total_simulation_in_progress_records: type: integer total_simulation_finished_records: type: integer total_records: type: integer progress: type: array minItems: 1 maxItems: 1 items: type: integer description: >- Single-element array containing the progress percentage (0-100). Quirky shape preserved for backwards compatibility. example: - 0 simulation_call_recording: type: array items: type: object additionalProperties: true can_stop: type: boolean active_calls_count: type: integer create_date: type: string example: 05/06/2026 13:31:54 example: success: true simulation: id: 456 name: Customer Support Test bot_id: id: 1234 name: Customer Support Agent scenarios_ids: [] status: Draft number_of_call_to_make: 1 concurrent_call_count: 3 max_call_duration_in_minutes: 3 what_went_wrong: false suggestions_for_improvement: false prompt_suggestion: false is_auto_prompt_suggestions_applied: false how_many_scenario_to_generate: 0 summary: false analyticsData: Positive: 0 Negative: 0 Neutral: 0 total_simulation_remaining_records: 0 total_simulation_in_progress_records: 0 total_simulation_finished_records: 0 total_records: 0 progress: - 0 create_date: 05/08/2026 09:10:29 simulation_call_recording: [] can_stop: false active_calls_count: 0 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Get details of a specific simulation simulation_id = 456 response = client.simulation.get(simulation_id) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/simulations/{simulation_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # List simulations (/docs/api-reference/simulation/listSimulations) > Retrieve simulations with pagination. **GET** `/simulations` Retrieve simulations with pagination. ```yaml operationId: listSimulations parameters: - name: pageno in: query description: Page number for pagination. schema: type: integer default: 1 - name: pagesize in: query description: Items per page (max 150). schema: type: integer default: 10 maximum: 150 responses: '200': description: Paginated list of simulations. content: application/json: schema: type: object properties: records: type: array items: type: object description: A test simulation for an agent. properties: id: type: integer example: 772 name: type: string bot_id: type: object description: The agent under test. properties: id: type: integer name: type: string status: type: string enum: - Draft - Pending - In Progress - Calculating Summary - Completed - Stopped number_of_call_to_make: type: integer concurrent_call_count: type: integer max_call_duration_in_minutes: type: integer scenarios_ids: type: array description: Scenario records attached to this simulation. items: type: object additionalProperties: true what_went_wrong: oneOf: - type: string - type: boolean suggestions_for_improvement: oneOf: - type: string - type: boolean prompt_suggestion: oneOf: - type: string - type: boolean is_auto_prompt_suggestions_applied: type: boolean how_many_scenario_to_generate: type: integer summary: oneOf: - type: string - type: boolean analyticsData: type: object properties: Positive: type: integer Negative: type: integer Neutral: type: integer total_simulation_remaining_records: type: integer total_simulation_in_progress_records: type: integer total_simulation_finished_records: type: integer total_records: type: integer progress: type: array minItems: 1 maxItems: 1 items: type: integer description: >- Single-element array containing the progress percentage (0-100). Quirky shape preserved for backwards compatibility. example: - 0 simulation_call_recording: type: array items: type: object additionalProperties: true can_stop: type: boolean active_calls_count: type: integer create_date: type: string example: 05/06/2026 13:31:54 total_records: type: integer example: records: [] total_records: 0 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # List all simulations with pagination response = client.simulation.list(pageno=1, pagesize=10) print(response) ``` **curl** ```bash curl -X GET "https://omnidim.io/api/v1/simulations" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Start simulation (/docs/api-reference/simulation/startSimulation) > Begin running a simulation. Optionally update scenarios at start time (same shape as Update simulation). **POST** `/simulations/{simulation_id}/start` Begin running a simulation. Optionally update scenarios at start time (same shape as Update simulation). ```yaml operationId: startSimulation parameters: - name: simulation_id in: path required: true schema: type: integer requestBody: content: application/json: schema: type: object properties: scenarios: type: array description: Optional array of scenarios to update before starting. items: type: object required: - name - description - expected_result properties: id: type: integer description: Include this to update an existing scenario; omit it to add a new one. name: type: string example: Customer asks about pricing description: type: string example: Caller pushes back on the monthly price. description: Updated instructions for the test scenario. expected_result: type: string example: Agent explains the tiers without discounting. description: Updated expected outcome from the agent. selected_voices: type: array description: Updated voice configurations for the test calls. items: type: object required: - id - provider properties: id: type: string description: Voice ID from the provider. provider: type: string enum: - eleven_labs - play_ht - deepgram - cartesia - rime responses: '200': description: Simulation started. content: application/json: schema: type: object properties: success: type: boolean message: type: string simulation: type: object description: A test simulation for an agent. properties: id: type: integer example: 772 name: type: string bot_id: type: object description: The agent under test. properties: id: type: integer name: type: string status: type: string enum: - Draft - Pending - In Progress - Calculating Summary - Completed - Stopped number_of_call_to_make: type: integer concurrent_call_count: type: integer max_call_duration_in_minutes: type: integer scenarios_ids: type: array description: Scenario records attached to this simulation. items: type: object additionalProperties: true what_went_wrong: oneOf: - type: string - type: boolean suggestions_for_improvement: oneOf: - type: string - type: boolean prompt_suggestion: oneOf: - type: string - type: boolean is_auto_prompt_suggestions_applied: type: boolean how_many_scenario_to_generate: type: integer summary: oneOf: - type: string - type: boolean analyticsData: type: object properties: Positive: type: integer Negative: type: integer Neutral: type: integer total_simulation_remaining_records: type: integer total_simulation_in_progress_records: type: integer total_simulation_finished_records: type: integer total_records: type: integer progress: type: array minItems: 1 maxItems: 1 items: type: integer description: >- Single-element array containing the progress percentage (0-100). Quirky shape preserved for backwards compatibility. example: - 0 simulation_call_recording: type: array items: type: object additionalProperties: true can_stop: type: boolean active_calls_count: type: integer create_date: type: string example: 05/06/2026 13:31:54 example: success: true message: Simulation started successfully simulation: id: 456 name: Customer Support Test status: Pending number_of_call_to_make: 1 concurrent_call_count: 3 max_call_duration_in_minutes: 3 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Start a simulation simulation_id = 456 response = client.simulation.start(simulation_id) print(response) # Start with updated scenarios response = client.simulation.start( simulation_id, scenarios=[ { "id": 789, "name": "Updated Order Pizza", "description": "Updated instructions...", "expected_result": "Updated expected behavior...", "selected_voices": [...] } ] ) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/simulations/{simulation_id}/start" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Stop simulation (/docs/api-reference/simulation/stopSimulation) > Stop a running simulation. **POST** `/simulations/{simulation_id}/stop` Stop a running simulation. ```yaml operationId: stopSimulation parameters: - name: simulation_id in: path required: true schema: type: integer responses: '200': description: | Stop attempted. `success: true` when the simulation was running and is now stopped; `success: false` (still HTTP 200) when the simulation isn't in a stoppable state; the `message` explains why. `disconnect_results` is only present on the success path. content: application/json: schema: type: object properties: success: type: boolean message: type: string disconnect_results: type: object description: Per-recording stop counts. Only present when `success` is true. properties: successful_disconnects: type: integer failed_disconnects: type: integer pending_stopped: type: integer example: success: true message: Simulation stopped successfully disconnect_results: successful_disconnects: 0 failed_disconnects: 0 pending_stopped: 0 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Stop a running simulation simulation_id = 456 response = client.simulation.stop(simulation_id) print(response) ``` **curl** ```bash curl -X POST "https://omnidim.io/api/v1/simulations/{simulation_id}/stop" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ``` # Update simulation (/docs/api-reference/simulation/updateSimulation) > Update an existing simulation. Pass the full `scenarios` array (existing entries you want to keep plus any changes). **PUT** `/simulations/{simulation_id}` Update an existing simulation. Pass the full `scenarios` array (existing entries you want to keep plus any changes). ```yaml operationId: updateSimulation requestBody: required: true content: application/json: schema: type: object properties: name: type: string example: Pricing objection run description: Name of the simulation for identification. agent_id: type: integer example: 158910 description: ID of the agent to test. number_of_call_to_make: type: integer minimum: 1 maximum: 3 description: Number of calls to make per scenario (default 1, max 3). concurrent_call_count: type: integer minimum: 1 maximum: 3 description: Number of concurrent calls to run (default 3, max 3). max_call_duration_in_minutes: type: integer minimum: 1 maximum: 10 description: Maximum duration for each call in minutes (default 3, max 10). scenarios: type: array description: >- Full scenario list. Include existing scenarios you want to keep, plus any new or updated ones. items: type: object required: - name - description - expected_result properties: id: type: integer description: Include this to update an existing scenario; omit it to add a new one. name: type: string description: Name of the test scenario. description: type: string description: Updated instructions for the test scenario. expected_result: type: string description: Updated expected outcome from the agent. selected_voices: type: array description: Updated voice configurations for the test calls. items: type: object required: - id - provider properties: id: type: string description: Voice ID from the provider. provider: type: string enum: - eleven_labs - play_ht - deepgram - cartesia - rime responses: '200': description: Updated simulation. content: application/json: schema: type: object properties: success: type: boolean simulation: type: object description: A test simulation for an agent. properties: id: type: integer example: 772 name: type: string bot_id: type: object description: The agent under test. properties: id: type: integer name: type: string status: type: string enum: - Draft - Pending - In Progress - Calculating Summary - Completed - Stopped number_of_call_to_make: type: integer concurrent_call_count: type: integer max_call_duration_in_minutes: type: integer scenarios_ids: type: array description: Scenario records attached to this simulation. items: type: object additionalProperties: true what_went_wrong: oneOf: - type: string - type: boolean suggestions_for_improvement: oneOf: - type: string - type: boolean prompt_suggestion: oneOf: - type: string - type: boolean is_auto_prompt_suggestions_applied: type: boolean how_many_scenario_to_generate: type: integer summary: oneOf: - type: string - type: boolean analyticsData: type: object properties: Positive: type: integer Negative: type: integer Neutral: type: integer total_simulation_remaining_records: type: integer total_simulation_in_progress_records: type: integer total_simulation_finished_records: type: integer total_records: type: integer progress: type: array minItems: 1 maxItems: 1 items: type: integer description: >- Single-element array containing the progress percentage (0-100). Quirky shape preserved for backwards compatibility. example: - 0 simulation_call_recording: type: array items: type: object additionalProperties: true can_stop: type: boolean active_calls_count: type: integer create_date: type: string example: 05/06/2026 13:31:54 example: success: true simulation: id: 456 name: Customer Support Test (Updated) bot_id: id: 1234 name: Customer Support Agent status: Draft number_of_call_to_make: 1 concurrent_call_count: 3 max_call_duration_in_minutes: 5 ``` **Python SDK** ```python from omnidimension import Client client = Client(api_key) # Update an existing simulation simulation_id = 456 update_data = { "name": "Updated Restaurant Test", "max_call_duration_in_minutes": 5, "scenarios": [ { "name": "Updated Order Pizza", "description": "Updated instructions...", "expected_result": "Updated expected behavior..." } ] } response = client.simulation.update(simulation_id, update_data) print(response) ``` **curl** ```bash curl -X PUT "https://omnidim.io/api/v1/simulations/{simulation_id}" \ -H "Authorization: Bearer $OMNIDIM_API_KEY" ```