# Get Clients Assigned to Agent Source: https://docs.voiceaiwrapper.app/api-reference/agents/get-clients GET /api/v2/agents/{agent_id}/clients Retrieves all clients associated with a specific assistant/agent, including their subscription and product details. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Authentication Source: https://docs.voiceaiwrapper.app/api-reference/authentication Learn how to authenticate your API requests ## API Keys All API requests (both v1 and v2) must be authenticated using an API key. You can generate and manage API keys from your campaign's dashboard. ## Generating an API Key Go to your Settings in the dashboard Image Click on the "API Keys" tab Image Click on the "Generate API Key" button Image Click "Generate API Key" and give it a descriptive name Image Copy and securely store your API key - you won't be able to see it again Image **Security**: Keep your API keys secure and never share them publicly. If a key is compromised, disable it immediately and generate a new one. ## Using Your API Key Include your API key in the `Authorization` header of every request: ```bash cURL theme={null} curl --location 'https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{...}' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY' }, body: JSON.stringify({...}) }); ``` ```python Python theme={null} import requests url = 'https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads' headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY' } response = requests.post(url, headers=headers, json={...}) ``` ## Finding Your Tenant ID (v1 Only) **v2 Users**: You can skip this section. v2 automatically determines your tenant ID from your API key. If you're using API v1, you'll need to include your `tenant_id` in request bodies. You can find it: 1. In your dashboard URL: `https://dashboard.voiceaiwrapper.app/en/{tenant-id}` 2. In the API documentation panel 3. By contacting support ## Managing API Keys ### Viewing API Keys All your API keys are listed in the API Keys tab with: * Name * Status (Active/Disabled) * Masked key value (for security) ### Enabling/Disabling Keys Toggle the switch next to each API key to enable or disable it instantly. Disabled keys cannot authenticate requests. ### Deleting API Keys Click the actions menu next to a key to permanently delete it. This action cannot be undone. **Best Practice**: Create separate API keys for different applications or environments (development, staging, production). This makes key rotation easier and improves security. ## Testing Your Authentication Try making a simple API call to verify your authentication is working: ```bash cURL theme={null} curl --location 'https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); console.log(response.status); // Should be 200 if authenticated ``` ```python Python theme={null} import requests response = requests.get( 'https://api.voiceaiwrapper.app/api/v2/voice-campaigns/{campaignId}/leads', headers={'Authorization': 'Bearer YOUR_API_KEY'} ) print(response.status_code) # Should be 200 if authenticated ``` ## Troubleshooting * Verify your API key is correct and hasn't been disabled * Check that you're including the "Bearer " prefix * Ensure the key hasn't been deleted * Confirm your API key has the necessary permissions * Verify you're accessing a campaign you have access to * Check that your tenant is active * Generate a new API key and try again * Verify you copied the entire key without spaces * Check that the key is enabled in the dashboard # Create Campaign Source: https://docs.voiceaiwrapper.app/api-reference/campaigns/create-campaign POST /api/v2/voice-campaigns Creates a new campaign for the authenticated tenant. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Update Campaign Source: https://docs.voiceaiwrapper.app/api-reference/campaigns/update-campaign PATCH /api/v2/voice-campaigns/{campaign_id} Partially updates an existing campaign. Only fields provided in the request body are updated. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Update Campaign Status Source: https://docs.voiceaiwrapper.app/api-reference/campaigns/update-campaign-status PATCH /api/v2/voice-campaigns/{campaign_id}/status Updates the status of a campaign to one of the allowed values. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Assign Resources to Client Account Configuration Source: https://docs.voiceaiwrapper.app/api-reference/client-account-configurations/assign-resources POST /api/v2/client-account-configurations/{client_account_configuration_id}/resources/assign Replaces all resource assignments for a client account configuration with the provided set. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Get Assigned Resources for Client Account Configuration Source: https://docs.voiceaiwrapper.app/api-reference/client-account-configurations/get-assigned-resources GET /api/v2/client-account-configurations/{client_account_configuration_id}/resources Retrieves all resources currently assigned to a client account configuration. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Get Form Submissions Source: https://docs.voiceaiwrapper.app/api-reference/client-forms/get-form-submissions GET /api/v2/client-forms/{client_form_id}/submissions Retrieves submissions for a client form, optionally filtered to a specific client. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Create Client Source: https://docs.voiceaiwrapper.app/api-reference/clients/create-client POST /api/v2/clients/create Creates a new client account under the authenticated tenant, including user accounts, a client account configuration, billing configuration, and access permissions. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Get Client Details Source: https://docs.voiceaiwrapper.app/api-reference/clients/get-client-details GET /api/v2/clients/{client_id} Retrieves comprehensive details for a single client, including subscription, usage metrics, assigned campaigns, and form submissions. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # List All Clients Source: https://docs.voiceaiwrapper.app/api-reference/clients/list-clients GET /api/v2/clients Returns all client accounts for the authenticated tenant. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Get Payment Request Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/get-payment-request GET /api/v2/external-billing/payment-requests/{payment_request_id}/ Returns a single payment request by ID. The payment request must belong to your tenant. ## Authentication Requires `Authorization: Bearer `. The API key must belong to a tenant on a **PRO plan or above**. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. # Get Subscription Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/get-subscription GET /api/v2/external-billing/subscriptions/{subscription_id}/ Retrieve a single subscription by its ID. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. # Get Usage Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/get-usage GET /api/v2/external-billing/usage Retrieve minute usage records for your tenant - included minutes, add-on balance, and billable overage. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. ## Combining filters All query parameters are optional and can be used together or individually: ``` GET /api/v2/external-billing/usage?client_id= GET /api/v2/external-billing/usage?client_id=&page=2&page_size=50 GET /api/v2/external-billing/usage ``` | Parameter | Optional | Notes | | ----------- | -------- | ----------------------------------------------------- | | `client_id` | Yes | Scope to a single client. Omit to return all clients. | | `page` | Yes | Defaults to `1`. | | `page_size` | Yes | Defaults to `20`, max `100`. | ## Understanding the usage fields | Field | What it means | | ------------------------ | -------------------------------------------------------------------------------------------------------------- | | `minutes_included_limit` | The total included minutes granted by the client's current plan for this billing period. Resets at period end. | | `minutes_included_used` | Included minutes consumed so far in the current period. | | `minutes_addon_balance` | Remaining balance of add-on pack minutes. **Does not reset** at period end - carries forward until exhausted. | | `minutes_billable_used` | Minutes consumed beyond both the included limit and the add-on balance. These will be billed as overage. | ## How minutes are consumed ``` Incoming call minute → first draws from minutes_included_limit → once exhausted, draws from minutes_addon_balance → once both exhausted, increments minutes_billable_used (overage) ``` Overage minutes result in a **Cycle - Usage** payment request at the end of the billing period if the plan has a per-minute overage rate configured. # List Payment Requests Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/list-payment-requests GET /api/v2/external-billing/payment-requests Retrieve a paginated list of payment requests for your tenant, with optional filters for client, status, and type. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. ## Combining filters All query parameters are optional and can be used together or individually: ``` GET /api/v2/external-billing/payment-requests?client_id=&status=PENDING&type=SUBSCRIPTION GET /api/v2/external-billing/payment-requests?status=OVERDUE GET /api/v2/external-billing/payment-requests?client_id=&type=ADDON GET /api/v2/external-billing/payment-requests?client_id= ``` | Parameter | Optional | Notes | | ----------- | -------- | ------------------------------------------------------------------------------ | | `client_id` | Yes | Scope to a single client. Omit to return all clients. | | `status` | Yes | Filter by payment status (`PENDING`, `PAID`, `FAILED`, `CANCELED`, `OVERDUE`). | | `type` | Yes | Filter by request type (`SUBSCRIPTION`, `ADDON`, `OVERAGE`, `ONE_TIME`). | | `page` | Yes | Defaults to `1`. | | `page_size` | Yes | Defaults to `20`, max `100`. | ## Payment request statuses | Status | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------ | | `PENDING` | Created, awaiting payment. | | `PAID` | Marked as paid. | | `OVERDUE` | Due date passed without payment. Triggers `past_due` on the linked subscription (cycle requests only). | | `FAILED` | Payment could not be completed. | | `CANCELED` | Canceled - no longer requires payment. | ## Payment request types | Type | Meaning | | -------------- | ------------------------------------------------------------------- | | `SUBSCRIPTION` | Recurring flat-fee charge for a billing period. | | `ADDON` | Charge for an add-on pack applied to a client. | | `OVERAGE` | Per-minute overage charge generated at the end of a billing period. | | `ONE_TIME` | One-off charge outside the normal billing cycle. | # List Subscriptions Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/list-subscriptions GET /api/v2/external-billing/subscriptions Retrieve a paginated list of client subscriptions for your tenant, with optional filters for client and status. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. ## Combining filters All query parameters are optional and can be used together or individually: ``` GET /api/v2/external-billing/subscriptions?client_id=&status=active GET /api/v2/external-billing/subscriptions?status=past_due GET /api/v2/external-billing/subscriptions?client_id= ``` | Parameter | Optional | Notes | | ----------- | -------- | ----------------------------------------------------- | | `client_id` | Yes | Scope to a single client. Omit to return all clients. | | `status` | Yes | Filter by subscription status. | | `page` | Yes | Defaults to `1`. | | `page_size` | Yes | Defaults to `20`, max `100`. | ## Subscription statuses | Status | Meaning | | ---------- | ---------------------------------------------------------------------------------------- | | `active` | Subscription is running normally. | | `past_due` | A payment request was not paid by its due date. Client portal and campaigns are blocked. | | `blocked` | Grace period expired without payment. Subscription must be canceled and restarted. | | `canceled` | Subscription was explicitly canceled. History is preserved. | # Update Payment Request Source: https://docs.voiceaiwrapper.app/api-reference/external-billing/update-payment-request PATCH /api/v2/external-billing/payment-requests/{payment_request_id}/update Update the status of a payment request. Any status transition is permitted. This endpoint requires a **PRO plan or above**. If your plan does not include the External Billing API, the request returns `403` with error code `external_billing_api`. ## What happens when you mark a request as `PAID` Sending `"status": "PAID"` triggers the internal payment service in addition to updating the status field: 1. **`paid_at`** is recorded with the current timestamp. 2. **`external_payment_id`** is stored if provided (useful as a reference to your payment processor's transaction ID). 3. The platform checks whether the linked subscription has any remaining **unpaid cycle requests** (`PENDING` or `OVERDUE`). * If none remain → subscription status returns to **`active`** and any paused campaigns are **automatically resumed**. * If others remain → subscription stays in its current state until all are resolved. ## `external_payment_id` is optional You can mark a request as `PAID` without providing `external_payment_id`. It is recommended when you have a transaction reference from your payment processor (e.g. `txn_abc123`, `pi_abc123` from Stripe), but the field is not required. ## Status reference | Status | When to use | | ---------- | ------------------------------------------------------------------------------------------- | | `PAID` | Client has paid. Triggers subscription recovery if no other cycle requests are outstanding. | | `PENDING` | Reset to pending (e.g. if you marked paid by mistake). | | `FAILED` | Payment attempt failed - client should be contacted. | | `CANCELED` | Charge is void - no payment needed. | | `OVERDUE` | Mark manually overdue (normally set automatically by the platform's sweep). | Setting a cycle request to `PAID` can change the subscription status and resume paused campaigns. Verify you are acting on the correct payment request before proceeding. # Add Lead to Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v1/add-to-voice-campaign POST /v1/api/voice-campaigns/{campaign_id}/leads Adds a single lead to the specified campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Remove Lead from Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v1/remove-from-voice-campaign DELETE /v1/api/voice-campaigns/{campaign_id}/lead/remove Permanently removes a lead from the specified campaign, identified by phone number or campaign lead ID. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Update Lead in Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v1/update-in-voice-campaign PATCH /v1/api/voice-campaigns/{campaign_id}/lead/update Updates fields on an existing lead within the specified campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Add Lead to Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/add-to-voice-campaign POST /api/v2/voice-campaigns/{campaign_id}/leads Adds a single lead to the specified campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Bulk Add Leads to Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/bulk-add-to-voice-campaign POST /api/v2/voice-campaigns/{campaign_id}/leads/bulk Queues multiple leads for asynchronous addition to the specified campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Bulk Remove Leads from Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/bulk-remove-from-voice-campaign DELETE /api/v2/voice-campaigns/{campaign_id}/leads/remove/bulk Removes multiple leads from the specified campaign in a single request. Each item is processed individually and per-lead results are returned. Each item must include either a `phone_number` or a `campaign_lead_id`. When both are provided, `campaign_lead_id` takes precedence. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Bulk Update Leads in Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/bulk-update-in-voice-campaign PATCH /api/v2/voice-campaigns/{campaign_id}/leads/update/bulk Updates multiple existing leads in the specified campaign in a single request. Each item is processed individually and per-lead results are returned. `phone_number` is required in every item to identify the lead. Only provided fields are changed. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # List Leads in Voice Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/get-campaign-leads-list GET /api/v2/voice-campaigns/{campaign_id}/leads/list Returns all leads for the specified campaign, ordered by most recently created first. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Remove Lead from Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/remove-from-voice-campaign DELETE /api/v2/voice-campaigns/{campaign_id}/lead/remove Permanently removes a lead from the specified campaign, identified by phone number or campaign lead ID. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Update Lead in Campaign Source: https://docs.voiceaiwrapper.app/api-reference/leads-v2/update-in-voice-campaign PATCH /api/v2/voice-campaigns/{campaign_id}/lead/update Updates fields on an existing lead within the specified campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Create VoiceAIPod Source: https://docs.voiceaiwrapper.app/api-reference/voice-providers/create-voice-provider POST /api/v2/voiceai-pod Registers a new voice provider integration for the authenticated tenant. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Create Web Widget Source: https://docs.voiceaiwrapper.app/api-reference/web-widgets/create-web-widget POST /api/v2/web-widgets Creates a new web widget for an inbound campaign. ## Authentication All requests require an `Authorization` header using Bearer token authentication: `Authorization: Bearer ` # Add-On Packs Source: https://docs.voiceaiwrapper.app/documentation/billing/addon-packs How to create add-on packs, assign them to clients, and credit minutes to a client's wallet. ## What is an add-on pack? An add-on pack is a one-off block of minutes that a client can purchase on top of their plan's included allowance. Unlike included minutes, add-on minutes go into the client's **wallet** and never expire - they are used after included minutes are exhausted and carry over across billing periods until spent. **Examples from the UI:** * "100 Minutes Pack" - 100 minutes for a fixed price. * Any custom name, description, minute count, and price you configure. Add-on packs are only available to clients whose subscription plan has **Add-on packs · On**. *** ## Requirements The **Addon Pack Requirements** notice in the UI states: > *"Your client can purchase addon packs when their subscription product has addon packs enabled. The client will only see addon packs that you assign here."* This means two things must be true before add-on packs work: 1. The client's assigned plan must show **Add-on packs · On** (the plan's add-on setting). 2. You must assign at least one pack to the client in the **Addon Packs** tab. If the plan has **Add-on packs · Off**, the tab shows an amber warning: > *"Add-on packs are disabled for this subscription. The plan snapshot for this client has add-ons turned off. Turn on add-ons on the billing plan product (or re-subscribe after changing the plan), then refresh - you cannot assign packs or apply credits until add-ons are enabled on the plan."* *** ## Assigning packs to a client On the client's **Billing** tab, select the **Addon Packs** sub-tab. If no packs are assigned yet, you see **"Assign Addon Packs"** with a multi-select dropdown: **Select addon packs...** Each option is formatted as: ``` Pack name - {price} ({count} minutes) ``` Select one or more packs and click **Assign Addon Packs**. Assigned packs appear in the **Currently assigned to client** list and become visible in the client portal under **Available Add-On Packs**. The client sees a note: *"Contact your provider to purchase any of these add-on packs."* To change the assigned packs, click **Edit** on the assignment card. You can also click **Remove Assignment** next to any individual pack to unassign it. *** ## Applying a pack / crediting the wallet Crediting a pack is the act of immediately adding the pack's minutes to the client's wallet and creating a payment request for the pack amount. On an assigned pack card (in the **Addon Packs** tab), click the **Apply Addon Pack** button. This button only appears when the client has an active subscription. The dialog **"Apply addon pack to client?"** shows the pack name and the minute count to be credited, then lists what applying will do: * **"Credit the selected addon pack minutes to this client's wallet immediately."** * **"Set the payment request due date per the plan's 'Payment due after period start' setting (e.g. 1 day after application)."** * **"Create a payment request for this addon pack."** You then choose the payment request status: * **Pending** - the pack is credited now; the payment request stays open until you mark it paid. * **Paid** - the pack is credited and the payment request is immediately recorded as paid. Click the button. The wallet balance updates immediately and a new **Pack** payment request appears in the **Payment requests** table. * Success when marked **Pending**: *"Addon pack credited. Payment request created as pending."* * Success when marked **Paid**: *"Addon pack credited. Payment request created and marked as paid."* The wallet is credited immediately regardless of payment status. If the client's portal was blocked due to overage (all free minute pools exhausted), crediting a pack can restore access and resume campaigns automatically. *** ## Wallet behavior The client's **Add-on minutes (wallet)** section on the agency card and the **Add-on balance** line on the client's usage card both show the current balance. * **"Never expires"** - wallet minutes do not reset at period end. * Wallet minutes are consumed after included minutes are exhausted. * If the plan also has a per-minute rate, wallet minutes are drawn down before any billable excess accrues. * The balance persists across subscription renewals as long as the subscription stays active. # Billing Plans Source: https://docs.voiceaiwrapper.app/documentation/billing/billing-plans How to create plans, assign them to clients, and start a subscription. ## What a plan defines A billing plan is the template that drives everything: how much a client pays, how often, how many minutes they get, whether they can buy extra minutes, and whether usage above the included limit is billed per minute. When you start a subscription, the plan's terms at that moment are **snapshotted** onto the subscription. Editing a plan later does not change an already-running subscription. ### Plan attributes shown in the UI The display name shown to you and your client (for example, "Starter", "Growth", "100-Minute Pack"). How often the billing period repeats. A count greater than one produces labels like "2× Monthly". The currency for all amounts on this plan (ISO 4217 code, or a non-standard code such as USDT). This is snapshotted on the subscription and cannot be changed mid-subscription. When enabled, the client is charged a flat fee each billing period. When disabled, no fixed fee applies (usage-only plans). **Badge states on plan and subscription cards:** * **Recurring · / ** - shown when enabled. * **Recurring · Off** - shown when disabled. The fixed amount charged each billing period. For example, `49.00`. How many minutes the client gets each period at no extra charge. Shown in the badge as **"Minutes included: "**. Unused minutes do **not** carry over to the next period. When enabled, minutes used beyond the included allowance (and any add-on wallet balance) are billed at a per-minute rate. When disabled, no per-minute charge applies. **Badge states on plan and subscription cards:** * **Usage · /min** - shown when enabled. * **Usage · Off** - shown when disabled. The rate charged per minute of excess usage. For example, `0.10`. Whether this plan allows clients to purchase add-on minute packs. Must be **On** to assign or apply any add-on packs to a client on this plan. When chat is enabled, included minutes and usage are measured in chat equivalent minutes. The displayed badge **"1 min = chats"** shows the conversion rate. How many days after the start of a billing cycle the payment request becomes overdue. How many additional days after the due date the client has before the subscription is blocked. At least one of the recurring fee or usage rate must be set. A plan can also have both (a base fee plus per-minute overage beyond the included allowance). *** ## Assigning a plan to a client Before a subscription can start, you must assign a plan to the client. Open the client record and select the **Billing** tab. If the client uses External Billing, you see the **Current Plan** section. If no plan is assigned, the section shows **"Assign a Billing Plan"** with a dropdown: **Select a plan...** Each option in the dropdown is formatted as: ``` Plan name - {recurring amount} · {per-minute rate} · {rhythm} ``` Select the plan and click **Assign Plan**. After assigning, the card shows: * **"Currently assigned to client"** with a green checkmark. * The plan name, description, and badge summary (Recurring, Minutes included, Usage, Add-on packs, Chat). * The exact pricing block. * A **Start subscription** button. The client can now log in and see the assigned plan in their portal. To remove the assignment, click **Remove Assignment** (the trash icon in the top-right of the assignment card). This removes the plan from the client but preserves any existing subscription history. *** ## Starting a subscription You can start a subscription on behalf of the client, or the client can initiate it from their portal. On the assignment card, click the **Start subscription** button. A dialog appears: **"Start subscription for this client?"** It shows the plan name, price per period, and included minutes, then lists what starting will do: * **"Activate the subscription with a billing period beginning now."** * **"Initialize usage tracking (included minutes and add-on balance) for this plan."** * **"Create a payment request for this billing cycle when applicable."** Click **Start subscription** to confirm. The card switches to **"Current subscription"** and shows: * The current status badge (**Active**, **Past Due**, or **Blocked**). * The snapshotted **Plan terms** (the pricing and settings locked at start time). * **Billing period** - the start and end dates, plus a renewal badge. * **Payment due after period start** and **Grace after payment due** values. * Usage sections: included minutes progress, add-on wallet, and excess minutes if applicable. A first payment request is created automatically when there is a recurring fee. *** ## Plan terms snapshot When a subscription starts, the exact plan terms (price, rhythm, included minutes, per-minute rate, add-on eligibility, chat conversion) are locked in as a **snapshot**. The card shows: > *"Terms below reflect what was active when this subscription started."* This means you can edit your plan catalog freely without disrupting running subscriptions. To apply new terms to a client, cancel the current subscription and start a new one. # Client Portal Source: https://docs.voiceaiwrapper.app/documentation/billing/client-portal What clients see on their Billing page and how to read each section. ## Overview When a client user logs in and navigates to **Billing**, they see the **External Billing** page. The page is read-only from the client's perspective: they can view their plan, track usage, see available add-on packs, and review their payment history. All changes (starting subscriptions, applying packs, marking payments) are done by the agency. The page subtitle reads: *"Your current plan, usage, and available add-ons."* *** ## Status banners Banners appear at the top of the page when there is a billing issue. They are shown in order of severity. ### Outstanding balance (soft warning) Shown when the subscription is **Active** but there are unpaid amounts that have not yet become overdue: > **Outstanding balance** > *"You have an unpaid invoice on your account. Please contact your provider to arrange payment."* The unpaid amounts by currency are listed below the message. ### Payment overdue - access restricted Shown when the subscription is **Past Due**: > **Payment overdue - access restricted** > *"There is an outstanding payment on your account. Your access and campaigns are paused. Please contact your provider to resolve this."* ### Subscription suspended Shown when the subscription is **Blocked**: > **Subscription suspended** > *"Your subscription has been suspended due to an unpaid balance. Please contact your provider to restore access."* *** ## Current Plan card The **Current Plan** card shows the client's active subscription details. The status badge in the top-right corner shows **Active**, **Past Due**, or **Blocked** with a tooltip explaining what each means: * **Active** - *"Subscription is active and in good standing."* * **Past Due** - *"Payment is overdue. Your access and campaigns are paused until payment is received."* * **Blocked** - *"Grace period has expired. Contact your provider to restore access."* ### Pricing grid Inside the card, a two-column grid shows the plan's snapshotted values: | Left | Right | | -------------------- | ----------------------------------------------------------------- | | **Recurring fee** | Amount + currency + per period | | **Included minutes** | Number of minutes per period (with chat equivalent if applicable) | | **Overage rate** | Per-minute rate + currency (with chat equivalent if applicable) | ### Billing period At the bottom of the card: **"Billing period: – "** *** ## Usage This Period card Appears alongside the **Current Plan** card. Shows the client's consumption for the current billing period. ### Included minutes A progress bar with the label: ``` {used} used · {remaining} remaining ``` Below the bar: `{used} / {limit} minutes` If chat is enabled, a chat-equivalent line is shown in blue: `≈ {used} / {total} chats` ### Add-on balance Shown only when the balance is greater than zero: ``` Add-on balance {N} minutes ``` ### Billable usage Shown only when billable minutes have accrued (minutes used beyond included + wallet, with a per-minute rate): ``` Billable usage {N} minutes ``` ### Period note At the bottom: *"Period: – "* If there is no usage recorded yet: *"No usage recorded yet."* *** ## Available Add-On Packs Appears only when the subscription plan has **Add-on packs · On** and the agency has assigned at least one pack. Heading: **Available Add-On Packs** Sub-heading: *"Contact your provider to purchase any of these add-on packs."* Each pack is shown as a card with: * Pack name * Description (if set) * Price and currency * Number of minutes Clients cannot purchase packs directly from their portal - they contact the agency, and the agency applies the pack from the admin view. *** ## Payment requests table At the bottom of the page, a table titled **Payment requests** shows all charges. The description reads: > *"Invoices and charges for your plan and add-ons. Contact your provider to pay or if you have questions. Use the info icon next to status for due dates and grace details."* ### Columns | Column | What it shows | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Created** | Date and time the request was created, plus last updated time | | **Request** | Type (Cycle or Pack) badge, Kind (Recurring or Usage) badge, subscription name, canceled-sub badge if applicable, line item summary, period covered | | **Product / pack** | The billing plan or add-on pack name the request is for | | **Amount** | The charge amount | | **Due & paid** | Due date; "Grace until " for cycle requests; paid date once paid | | **Status** | Badge (Pending, Paid, Overdue, Failed, Canceled) with an info tooltip | ### Status info tooltip Clicking the info (**i**) icon next to any status opens a tooltip with: * A plain-English explanation of what the status means. * The **Due** date. * The **Grace ends** date (for Cycle requests only). For Pack requests: *"Grace periods apply to subscription cycle charges, not add-on pack purchases."* # External Billing Overview Source: https://docs.voiceaiwrapper.app/documentation/billing/overview How External Billing works and who it is for. ## What is External Billing? External Billing is the module that lets your agency charge clients directly for their usage of Voice AI Wrapper - outside of any platform-level subscription you may have. You define your own plans, set your own prices, and collect payment however you prefer. The platform tracks minutes, enforces access rules, and keeps a full payment history; you handle the actual collection. External Billing is separate from the internal Stripe billing that covers your agency's own platform subscription. It is entirely about what you charge **your clients**. ## How agencies use it The overall workflow has three phases: 1. **Set up your catalog** - Create one or more billing plans and, optionally, add-on packs in External Billing configuration. Plans define the price, billing rhythm, included minutes, and whether overage or add-on packs are allowed. 2. **Assign a plan to a client** - On the client's **Billing** tab, pick a plan from your catalog and assign it. Once a plan is assigned, the client can see it in their portal and you can start a subscription on their behalf. 3. **Manage the subscription and payments** - Once a subscription is active, the platform automatically generates payment requests on each billing cycle and tracks usage. You manually mark payment requests as paid (or failed/canceled) after you collect payment through your preferred channel. ## Key concepts | Term | What it means in the UI | | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Plan** | A billing product you create: price, interval, included minutes, overage rate, and add-on pack eligibility. | | **Subscription** | An active billing relationship between your agency and one client, based on a plan. Tracks the current period and status. | | **Payment request** | A single invoice line for a billing cycle (type **Cycle**) or an add-on pack purchase (type **Pack**). | | **Add-on pack** | A one-time block of minutes a client can purchase on top of their plan's included minutes. | | **Wallet** | The accumulated add-on minutes balance for a client. Wallet minutes never expire and do not reset at period end. | | **Billing period** | The date range for the current subscription cycle. Included minutes reset when each period ends. | | **Client portal** | The Billing page your clients see when they log in: current plan, usage, available add-on packs, and their payment history. | ## How the pieces connect Subscription Billing Flow: agency creates a plan, assigns it to the client, starts subscription, platform tracks usage; when the period ends, a new cycle payment request is created, the agency marks it paid, and the next period begins. Add-on path: from start subscription, agency applies add-on pack, pack payment request created, then agency marks as paid. ## What the client sees Clients log in to their portal and see: * Their **Current Plan** name, pricing, and billing period. * **Usage This Period** - how many included minutes they have used, any add-on balance, and any billable excess. * **Available Add-On Packs** - packs you have assigned to them, with a note to contact you to purchase. * **Payment requests** - a full table of all charges, their status, and due dates. If a payment is overdue or the subscription is blocked, prominent banners appear and all running campaigns are paused automatically. # Payment Requests Source: https://docs.voiceaiwrapper.app/documentation/billing/payment-requests The full reference for payment request types, kinds, statuses, due dates, grace periods, and what happens when a request is paid. ## What is a payment request? A payment request is the system's record of a billing charge. Every charge - whether for a recurring subscription fee, end-of-period usage, or an add-on pack - appears as a payment request in the **Payment requests** table. The table shows a chronological history, newest first. The agency sees the table with an **Update status** action on every row. Clients see the same table (read-only) with an info tooltip on each status. *** ## Two types: Cycle and Pack The **Type** badge on each row tells you what generated the request. ### Cycle A **Cycle** request is always linked to a subscription. It covers a billing period and is generated automatically by the platform. Cycle requests have a secondary **Kind** badge: | Kind | When it appears | | ------------- | ----------------------------------------------------------------------------------------------------- | | **Recurring** | Generated at the start of a billing period for the plan's flat fee. | | **Usage** | Generated at the end of a billing period for per-minute charges accrued above the included allowance. | Cycle requests have both a **due date** and a **grace period end date**. The "Grace until " note appears in the **Due & paid** column for cycle rows only. ### Pack A **Pack** request is created when you apply an add-on pack to a client. It is linked to the add-on pack, not the billing period. Pack requests have a **due date** but **no grace period** - the tooltip on the client side states: > *"Grace periods apply to subscription cycle charges, not add-on pack purchases."* *** ## Cycle vs Pack at a glance Subscription lifecycle state diagram: Start leads to Active (campaigns run, portal open). Active renews each period, can become Past due if payment is not paid by the due date, or Canceled if the agency cancels. Past due (campaigns paused, portal blocked) can return to Active when marked as paid (dashed arrow), move to Blocked when the grace period expires, or to Canceled if the agency cancels. Blocked (must cancel and restart) can only go to Canceled. Canceled is terminal. Legend: green Active, orange Past due, red Blocked (unrecoverable without canceling), gray Canceled. *** ## Payment request statuses | Status | Badge color | Meaning | | ------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- | | **Pending** | Amber | The charge has been created and is awaiting payment. | | **Paid** | Green | The request has been marked paid by you (or auto-marked when you apply a pack). | | **Overdue** | Red | The due date passed without the request being marked paid. For Cycle requests, this moves the subscription to **Past Due**. | | **Failed** | Red | The payment could not be completed. Shown for exceptional cases; contact the client for next steps. | | **Canceled** | Muted/grey | The request was canceled and no longer requires payment. | ### What the status tooltips tell clients *"This invoice is open. Please arrange payment with your provider before the due date. For subscription charges, a grace period may apply after the due date until payment is received."* *"Payment was not received by the due date. Your provider may restrict access or pause usage until the balance is cleared."* *"This payment could not be completed. Contact your provider for next steps."* *"This request has been marked paid by your provider."* *"This request was canceled and no longer requires payment."* *** ## Due dates and grace periods Every payment request has a **Due** date. For **Cycle** requests, a **Grace until ** date is also shown. These are calculated from the subscription's snapshotted settings at start time: * **Payment due after period start** - N days after the billing period opens, the Cycle recurring request becomes overdue if not paid. * **Grace after payment due** - N additional days after the due date before the subscription moves from **Past Due** to **Blocked**. Pack requests have a due date - set to the plan's **Payment due after period start** number of days after the pack is applied (for example, with the default of 1 day, the due date falls the day after application) - but no grace period, and do not affect the subscription status when overdue. *** ## Status transition: Overdue and recovery Subscription lifecycle state diagram: Start leads to Active (campaigns run, portal open). Active renews each period, can become Past due if payment is not paid by the due date, or Canceled if the agency cancels. Past due (campaigns paused, portal blocked) can return to Active when marked as paid (dashed arrow), move to Blocked when the grace period expires, or to Canceled if the agency cancels. Blocked (must cancel and restart) can only go to Canceled. Canceled is terminal. Legend: green Active, orange Past due, red Blocked (unrecoverable without canceling), gray Canceled. *** ## Updating a payment request status Agencies update statuses manually from the **Payment requests** table. Open the client's **Payment Requests** tab. Requests are shown newest-first with type, kind, product/pack name, amount, line-item breakdown, due date, and current status. In the **Actions** column, click **Update status**. The dialog **"Update payment status"** shows: > *"Change the status for this payment request ()."* Select the new status and click **Save**. A confirmation toast appears on success. *** ## What happens when a Cycle request is marked Paid * The platform checks whether any other **Cycle** requests for the same subscription remain unpaid (Pending or Overdue). * If none remain, the subscription returns to **Active**. * If the subscription was **Past Due**, any campaigns that were paused due to billing are **automatically resumed**. * The **"Paid"** timestamp and the name of the person who marked it are recorded and shown in the **Due & paid** column as **"Marked by "**. *** ## Amount breakdown Each payment request can have multiple line items. When a row has line items, the **Amount** cell is clickable (underlined) and shows a hover card titled **"Amount breakdown"** that lists each line with its description and, where applicable, the number of minutes it covers. **Example - combined Recurring + Usage cycle invoice:** | Line | Description | Amount | | ------------- | ------------------------------- | ----------- | | Recurring fee | Flat fee for the billing period | \$49.00 | | Usage | 23 min at \$0.10/min | \$2.30 | | **Total** | | **\$51.30** | **Example - Pack payment request:** | Line | Description | Amount | | ----------- | ---------------- | ----------- | | Add-on pack | 100 Minutes Pack | \$20.00 | | **Total** | | **\$20.00** | *** ## Canceled subscription badge If the subscription linked to a payment request was later canceled, the row shows a **"Subscription canceled"** badge with a tooltip: > *"The subscription linked to this request was canceled or removed; this row is kept for history."* This is informational - the history is always preserved even after a subscription is canceled. # Subscription Lifecycle Source: https://docs.voiceaiwrapper.app/documentation/billing/subscription-lifecycle Every state a client subscription can be in, what triggers each transition, and what it means for the client. ## Subscription statuses A subscription is always in one of four states. The current status is shown as a badge on both the agency's **Current subscription** card and the client's **Current Plan** card. | Status | Badge style | Meaning | | ------------ | -------------------- | ---------------------------------------------------------------------------------------------------------- | | **Active** | Default (blue/brand) | Subscription is active and in good standing. Campaigns run normally. | | **Past Due** | Destructive (red) | A payment request was not paid by its due date. Portal blocked; campaigns paused. | | **Blocked** | Destructive (red) | The grace period expired without payment. The subscription cannot recover; must be canceled and restarted. | | **Canceled** | Secondary (muted) | The subscription was explicitly canceled. Historical payment requests are preserved. | *** ## Lifecycle diagram Subscription lifecycle state diagram: Start leads to Active (campaigns run, portal open). Active renews each period, can become Past due if payment is not paid by the due date, or Canceled if the agency cancels. Past due (campaigns paused, portal blocked) can return to Active when marked as paid (dashed arrow), move to Blocked when the grace period expires, or to Canceled if the agency cancels. Blocked (must cancel and restart) can only go to Canceled. Canceled is terminal. Legend: green Active, orange Past due, red Blocked (unrecoverable without canceling), gray Canceled. *** ## Active The subscription is running normally. * The billing period runs between **Period start** and **Period end** (shown under **Billing period** on both the agency and client cards). * Included minutes reset at the end of each period. Unused minutes do **not** carry over. * The renewal badge shows one of: * **"Resets in days"** - if fewer than 7 days remain. * **"Period ends soon"** - if the period end is today or in the past. * **"Will renew on "** - for everything else. * At period end the platform automatically opens the next billing period and creates a new **Cycle** payment request (if the plan has a recurring fee or usage billed in arrears). *** ## Past Due **What triggers it:** The platform runs an hourly sweep. When a **Cycle** payment request passes its **Payment due after period start** deadline without being marked paid, the request moves from **Pending** to **Overdue** and the subscription moves from **Active** to **Past Due**. **What happens:** * The client portal shows the banner: **"Payment overdue - access restricted"** with the message *"There is an outstanding payment on your account. Your access and campaigns are paused. Please contact your provider to resolve this."* * All of the client's active campaigns are **paused automatically**. * The agency's subscription card shows a red banner: **"Payment overdue - access blocked"** with the note *"Mark the outstanding payment request as paid to restore access and resume campaigns automatically."* **How to recover:** Go to the client's **Payment requests** tab, find the overdue request, click **Update status**, and change the status to **Paid**. Once all outstanding cycle payment requests are marked paid, the subscription returns to **Active** and paused campaigns resume automatically. *** ## Blocked **What triggers it:** The platform's hourly sweep runs again after the **Grace after payment due** window passes. If the subscription is still **Past Due**, it moves to **Blocked**. **What happens:** * The agency's card shows the red banner: **"Subscription blocked - grace period expired"** with the note *"The grace period has expired. Cancel this subscription and start a new one to reactivate the client. Outstanding payment request history is preserved."* * The client sees: **"Subscription suspended"** - *"Your subscription has been suspended due to an unpaid balance. Please contact your provider to restore access."* * Campaigns remain paused. **How to recover:** Blocked subscriptions cannot be restored. You must: 1. Click **Cancel subscription** on the subscription card. 2. In the confirmation dialog you will see: *"This subscription is blocked due to an expired grace period. Canceling will delete the subscription and usage tracking so you can start a fresh one for the client. Outstanding payment request history will be preserved."* 3. Confirm, then start a new subscription for the client. *** ## Canceled Canceling ends the subscription and deletes usage tracking (included minutes counter, add-on balance). All historical payment requests are kept for your records. **When the subscription is not blocked**, the cancel confirmation reads: > *"This will immediately cancel the subscription and delete usage tracking."* **When the subscription is blocked**, the confirmation adds context about fresh start behavior. The **Cancel subscription** button appears whenever the subscription is **Active**, **Past Due**, or **Blocked**. *** ## Automatic period renewal At the end of each billing period, the platform automatically: 1. Starts a new period with the same terms. 2. Resets included minutes. 3. Creates a new **Cycle - Recurring** payment request for the flat fee (if the plan has one). 4. If there was per-minute usage above the included allowance in the previous period, also creates a **Cycle - Usage** payment request for the overage amount. You do not need to do anything manually for renewals. New requests appear in the **Payment requests** table immediately. # Usage and Access Source: https://docs.voiceaiwrapper.app/documentation/billing/usage-and-access How minutes are consumed, when campaigns pause, and what triggers access blocks. ## Minute pools and consumption order When a call is made or a chat interaction occurs, the platform deducts from the client's minute pools in a fixed order: Subscription lifecycle state diagram: Start leads to Active (campaigns run, portal open). Active renews each period, can become Past due if payment is not paid by the due date, or Canceled if the agency cancels. Past due (campaigns paused, portal blocked) can return to Active when marked as paid (dashed arrow), move to Blocked when the grace period expires, or to Canceled if the agency cancels. Blocked (must cancel and restart) can only go to Canceled. Canceled is terminal. Legend: green Active, orange Past due, red Blocked (unrecoverable without canceling), gray Canceled. The three pools are: ### Included minutes Minutes allocated fresh at the start of each billing period. Shown in the **Included minutes** section of the subscription card: * **"Available: / min"** * A progress bar that turns amber when 90% is used. * **"Resets each billing period"** tooltip note - *"These minutes are allocated each period and reset on . Unused minutes do not carry over."* * If the plan has no recurring fee, this section is hidden (usage-only plans have no included pool). ### Add-on balance (wallet) Minutes purchased through add-on packs. Shown as **"Add-on minutes (wallet)"** with the badge **"Never expires"**. * Drawn down only after included minutes are exhausted. * Only available if the subscription plan has **Add-on packs · On**. * Accumulated across multiple pack applications; balance persists across period renewals. * Clients see this as **"Add-on balance"** on their **Usage This Period** card. ### Excess minutes (billable) Minutes used beyond both the included allowance and the add-on wallet, when the plan has a per-minute rate configured. * Shown as **"Excess minutes (billable)"** on the subscription card. * The tooltip reads: *"Usage beyond included + add-on. Billed at per minute at period end."* * At period end, the platform creates a **Cycle - Usage** payment request for the total billable minutes × the per-minute rate. * Clients see this as **"Billable usage"** on their usage card. *** ## When there is no overage option If all three pools are exhausted and the plan has neither a per-minute rate nor any add-on balance, the platform treats this as an access restriction: * All of the client's campaigns are **paused automatically**. * The pause reason stored internally is: *"Included Minutes are exhausted; campaigns were paused to avoid further usage."* *** ## Chat conversion When **Chat Enabled** is shown on a plan, chat interactions count against the minute pools using the conversion rate displayed as **"1 min = chats"**. For example, if the conversion is 5 chats per minute, consuming 50 chats deducts 10 minutes from the active pool. The client's usage card shows equivalent chat counts wherever minute amounts appear: * *"≈ / chats"* below the included minutes progress bar. * Included minutes and overage rate descriptions include a blue annotation like *"(≈ chats)"* or *"(or chats)"*. *** ## Access gates and the client portal The platform checks several conditions before allowing a client user to access their portal or run campaigns: | Condition | Portal state | | ----------------------------------------------- | -------------------------------------------------------------------------------------- | | Client is disabled | Blocked | | Client is not on External Billing | Not applicable (different portal flow) | | Outstanding unpaid cycle balance | Soft warning banner (portal still open if not overdue) | | Subscription is **Past Due** | Blocked - *"Payment overdue - access restricted"* | | Subscription is **Blocked** | Blocked - *"Subscription suspended"* | | Plan assigned but no subscription started | Blocked - client sees a **Subscribe** button and can start the subscription themselves | | Subscription **Active** and pools not exhausted | Open | **Past Due** and **Blocked** statuses immediately restrict the client's portal access regardless of remaining minute balances. Marking the outstanding payment request as **Paid** is the only way to restore access from a Past Due state. *** ## Billing status summary badges On the agency's subscription card, two badges reflect the current billing state at a glance: * **" unpaid"** (red destructive badge) - appears when there are outstanding payment request amounts across any currency. * **"Next due: "** (outline badge) - shows the earliest upcoming due date across all open payment requests. *** ## Campaign pause and resume behavior When campaigns are paused by the billing system, the pause reason is one of: * **Minutes exhausted** - included and add-on balance both ran out, no per-minute rate. * **Subscription payment overdue** - the subscription moved to **Past Due**. * **Grace period expired** - the subscription moved to **Blocked**. Campaigns are **resumed automatically** when: * The subscription returns to **Active** (all cycle requests marked paid), **and** * At least one minute pool is still available (included minutes remain, wallet has balance, or a per-minute rate is configured). If campaigns were paused because minutes were exhausted, they do not resume until minutes are replenished (via a new period that resets included minutes, or an add-on pack being applied). # Overview Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode Hide the floating widget completely and start AI voice calls or chat from your own buttons Headless Mode lets you hide the floating widget completely and start AI voice calls or chat from your own buttons - your website keeps its design while the widget does the work invisibly in the background. Embed the widget with the `hide-launcher` attribute, drive it with imperative methods (`startCall()`, `sendChatMessage()`, …) and react to namespaced DOM events (`voice.*`, `chat.*`, `widget.*`). The widget renders nothing - your page owns 100% of the UI. **Pro feature:** Headless Mode is available on our Pro tiers only. To use `hide-launcher` and the JavaScript API, upgrade your plan from the [dashboard](https://dashboard.voiceaiwrapper.app). ## Why Headless Mode? You embed our widget on your website, but you may want the call or chat trigger to be *your own* branded button - a navbar link, a hero CTA, an image - not our floating launcher. Headless Mode removes our UI from the page entirely (even during an active call) and exposes a small, stable JavaScript API so any element on the page can become the trigger, and any part of the page can display the call or chat state. ## What you can rely on With `hide-launcher`, the widget renders **nothing - ever**, even in the middle of an active call. The API is **identical across Vapi, Retell, and ElevenLabs** - same methods, same events, same payloads, same ordering. You never branch on the provider. Calls and chat messages triggered before the widget finishes loading are **buffered**, never lost. Every started flow reaches a **guaranteed terminal event** - your state machine cannot get stuck. ## How it works * **No SDK, no npm package.** One script tag plus one custom element. The API is plain DOM methods and events, so it works in React, Vue, Svelte, vanilla JS, and Webflow custom code alike. * **Per-embed, not per-campaign.** `hide-launcher` is an attribute on the embed snippet. The same widget can be embedded normally on one page and headless on another. * **Voice and chat are independent.** One invisible widget can power a call button and a custom chat panel at the same time. ## Next steps A 5-minute integration: one attribute, five methods, copy-paste examples. `startCall()`, `stopCall()`, `sendChatMessage()`, `endChatSession()`, `acceptConsent()`. The full `widget.*`, `voice.*`, and `chat.*` event reference with ordering guarantees. How to show your own consent UI when "Require consent" is enabled. Complete, copy-paste HTML examples for voice, chat, and both together. Requirements, gotchas, and answers to common questions. # Consent in Headless Mode Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/consent Show your own consent UI when "Require consent" is enabled on a hidden widget Widgets can have **"Require consent"** enabled - a legal popup the visitor must accept before conversations start. Consent is stored in `localStorage` under the widget's configured key. * **Normal (visible) mode:** the widget shows its built-in consent popup; there is nothing for you to do. * **Headless Mode:** the widget renders nothing, so it cannot show its popup. Instead, it hands the consent step to you. In Headless Mode, **displaying the consent text is your responsibility.** If you have legal requirements for AI-call disclosure or recording notices, your own consent UI must satisfy them. ## The flow The visitor triggers `startCall()` or `sendChatMessage()`. Because consent hasn't been given yet, the widget **pauses** the action instead of executing it. The widget dispatches `widget.consent-required` with `{ pendingAction: 'voice' | 'chat' }` so you know what the visitor was trying to do. A modal, a checkbox, an inline banner - whatever fits your design. On accept, call `widget.acceptConsent()`: consent is persisted and **the paused action resumes automatically** - the call starts, or the queued message sends. On decline, do nothing; the action is simply dropped. ## Example ```js theme={null} const widget = document.querySelector('voiceai-widget'); const modal = document.getElementById('consent-modal'); widget.addEventListener('widget.consent-required', (e) => { // e.detail.pendingAction is 'voice' or 'chat' - useful for tailoring the copy modal.classList.add('show'); }); document.getElementById('consent-accept').onclick = () => { modal.classList.remove('show'); widget.acceptConsent(); // the paused call/message resumes automatically }; document.getElementById('consent-decline').onclick = () => { modal.classList.remove('show'); // declining = simply not calling acceptConsent() }; ``` ## Good to know * **Asked once per browser.** Consent is persisted in `localStorage`, so subsequent visits skip straight to the action. * **One flow covers both surfaces.** A single consent acceptance covers both voice and chat - you don't ask twice. * **`acceptConsent()` is always safe.** If consent wasn't requested, the call is a no-op. The [voice-and-chat example](/documentation/widget/headless-mode/examples) includes a complete consent modal serving both surfaces. # Events Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/events Every widget.*, voice.*, and chat.* event, with payloads and ordering guarantees All events are standard DOM `CustomEvent`s dispatched **on the `` element** (or on the `init()` container). Subscribe with `addEventListener` - it works in vanilla JS, React (via a ref), Vue, Svelte, Webflow custom code, anything. The payload is in `event.detail`. ```js theme={null} widget.addEventListener('voice.ended', (e) => { console.log('Call lasted', e.detail.durationMs, 'ms'); }); ``` Every `voice.*` / `chat.*` payload (and `widget.ready`) includes `provider: 'vapi' | 'retell' | 'elevenlabs'`. It is a **debug aid only** - keys and semantics are identical regardless of its value. Never branch on it. ## `widget.*` - lifecycle / shared | Event | Payload (`event.detail`) | Fires when | | ------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `widget.ready` | `{ provider, modes: ('voice'\|'chat')[] }` | Config fetched and parsed; all methods fully callable. `modes` says which surfaces this widget supports. | | `widget.consent-required` | `{ pendingAction: 'voice' \| 'chat' }` | Consent is enabled on the widget, the visitor hasn't consented yet, and a hidden widget can't show its built-in popup. Show your own consent UI, then call `acceptConsent()`. See [Consent in Headless Mode](/documentation/widget/headless-mode/consent). | | `widget.error` | `{ message: string }` | Config fetch failed, or a method was called for a mode the widget doesn't support. | ## `voice.*` - calls | Event | Payload | Fires when | | ----------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voice.connecting` | `{ provider }` | Start accepted (mic permission OK), provider connection beginning. | | `voice.started` | `{ provider }` | The call is live - audio flows both ways. | | `voice.ended` | `{ provider, durationMs: number }` | **Guaranteed terminal event** - fires exactly once per `voice.connecting`, for any ending: user hangup, agent hangup, error, even a failed connect (then `durationMs: 0`). `durationMs` is measured by the widget's own clock from `voice.started`. | | `voice.mic-permission-denied` | `{ provider }` | Browser microphone permission was rejected. Checked **before** any provider connection - fires *instead of* `voice.connecting` (no `voice.ended` follows). Identical behavior on all providers. | | `voice.error` | `{ provider, message: string, raw?: unknown }` | A call/SDK failure. `message` is always a human-readable string. `raw` is the untouched provider error - useful for debugging, but **explicitly not part of the stable contract** (its shape varies by provider and may change). | ## `chat.*` - messaging | Event | Payload | Fires when | | --------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `chat.session-started` | `{ provider, sessionId?: string }` | A chat session was created (auto-created by the first `sendChatMessage()`). | | `chat.message` | `{ provider, role: 'user'\|'assistant', content: string }` | A message entered the conversation. **Exactly one message per event.** Fires for the visitor's own messages too (`role: 'user'`), so the whole conversation can be rendered from this single stream. | | `chat.agent-typing-started` | `{ provider }` | The agent began composing a reply - show your typing indicator. | | `chat.agent-typing-stopped` | `{ provider }` | Always **balanced** with `-started`, and always fires **before** the assistant's `chat.message` and before `chat.session-ended` - typing dots can never get stuck. | | `chat.session-ended` | `{ provider, reason: 'user'\|'inactivity'\|'error'\|'hidden-tab', sessionId? }` | The session ended. The next `sendChatMessage()` starts a fresh session. See reasons below. | | `chat.error` | `{ provider, message: string, raw?: unknown }` | A send/session failure. Informational - does not replace `chat.session-ended`. | ### `chat.session-ended` reasons | Reason | Meaning | | ------------ | --------------------------------------------------------------------------------------------------------------------------------- | | `user` | The visitor (or your code via `endChatSession()`) ended it. | | `inactivity` | No activity for the widget's configured timeout (1–60 minutes, default 3). | | `hidden-tab` | The visitor switched tabs or minimized the browser; sessions auto-end to avoid zombie sessions. Identical on all three providers. | | `error` | The session died unexpectedly (network, provider). | When a session ends with `reason: 'inactivity'`, show something like "Session expired - send a message to start a new one." The next `sendChatMessage()` starts a fresh session automatically. **Chat sessions are ephemeral:** there is no history persistence across page loads, by design. ## Guarantees & event ordering These guarantees are enforced by an internal sequence guard - they do not depend on provider SDK behavior. ### Voice state machine (per call attempt) ``` idle ──startCall()──▶ [mic check] ├─ denied → voice.mic-permission-denied → idle (no ended) └─ granted → voice.connecting ├─ success → voice.started → … → voice.ended { durationMs } └─ failure → voice.error → voice.ended { durationMs: 0 } ``` * `voice.ended` fires **exactly once** per `voice.connecting` - never zero times, never twice. * Duplicate or out-of-order provider SDK events are dropped. * `voice.error` is informational and never replaces the terminal event. ### Chat turn sequence ``` [chat.session-started, if new] → chat.message{user} → chat.agent-typing-started → chat.agent-typing-stopped → ( chat.message{assistant} | chat.error ) ``` * The typing pair is always balanced; `-stopped` is guaranteed before `chat.session-ended`. * `chat.session-ended` fires exactly once per `chat.session-started`. * For streaming providers, assistant text streams internally, but the public stream still emits **one `chat.message` per completed reply** - identical to non-streaming providers. ### Cross-provider conformance The same integration code produces the same event names, payload keys, and ordering whether the campaign runs on Vapi, Retell, or ElevenLabs. Provider quirks - different SDK event names, batched replies, echoed user messages, missing typing signals, varying error shapes - are all normalized away before events reach your page. # Examples Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/examples Complete, copy-paste HTML pages for voice, chat, and both together Three self-contained pages, each driving an invisible widget with completely custom UI. Replace `YOUR_WIDGET_ID` and `your-domain.com` with the values from your campaign's **Web Widget tab → Embed Code** panel, serve over `https://` (or `http://localhost`), and they run as-is. These pages use no framework - plain HTML, CSS, and JavaScript - so the patterns translate directly to React, Vue, Svelte, or Webflow custom code. ## Voice call A single toggle button with full state cycling (Loading → Talk → Connecting → End), call duration from `durationMs`, mic-denied and error banners, and a consent listener. ```html voice-call.html [expandable] theme={null} Headless Mode - Voice Call Example

Talk to our AI agent

Press the button and start speaking - our assistant answers in real time.

``` ## Chat A complete custom chat panel: bubbles rendered from the single `chat.message` stream, balanced typing dots, an online/offline chip, an End-chat button, and human-readable handling of all four `session-ended` reasons. ```html chat.html [expandable] theme={null} Headless Mode - Chat Example

Chat with us

💬 Support Assistant offline
Send a message to start chatting
``` ## Voice + chat together One invisible widget powering both surfaces side by side. `widget.ready` → `e.detail.modes` gates each surface, and a single consent modal serves both flows. ```html voice-and-chat.html [expandable] theme={null} Headless Mode - Voice + Chat Example

Talk or chat - your choice

🎙 Voice

💬 Chat offline
Send a message to start chatting
``` # Methods Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/methods The imperative API: start calls, send chat messages, and manage consent from your own UI All methods exist in three places - use whichever fits your setup: 1. The `` DOM element 2. The object returned by `VoiceAIWidget.init()` 3. `window.VoiceAIWidget.*` (proxies to the first mounted instance) ```js theme={null} // All three are equivalent on a single-widget page: document.querySelector('voiceai-widget').startCall(); instance.startCall(); window.VoiceAIWidget.startCall(); ``` ## The readiness model The widget fetches its configuration asynchronously after the script loads. **You do not need to wait** - `startCall()` and `sendChatMessage()` are safe to call immediately: * A `startCall()` made before the widget is ready is **buffered** (one pending start) and fires right after `widget.ready`. * `sendChatMessage()` calls made before ready are **FIFO-queued** and flushed in order. The `widget.ready` event exists so careful integrators can gate their UI - for example, enabling the button only when the widget is ready. Its payload tells you which modes (`voice`, `chat`) the widget supports. ## At a glance | Method | What it does | | ----------------------------------------------- | -------------------------------------------------------- | | [`startCall()`](#startcall) | Starts a voice call. | | [`stopCall()`](#stopcall) | Ends the active call. | | [`sendChatMessage(text)`](#sendchatmessagetext) | Sends a chat message, auto-starting a session if needed. | | [`endChatSession()`](#endchatsession) | Ends the active chat session. | | [`acceptConsent()`](#acceptconsent) | Records consent and resumes the paused action. | ## `startCall()` Starts a voice call. ```js theme={null} widget.startCall(); ``` **Edge behavior:** * Called before the config loads → **buffered** (one pending start), fires right after `widget.ready`. * Called while a call is active or connecting → no-op plus a `console.warn`. * Voice not enabled on the widget (or no voice assistant configured) → fires `widget.error`. ## `stopCall()` Ends the active call. Also works for calls started from a visible launcher. ```js theme={null} widget.stopCall(); ``` **Edge behavior:** no-op when idle. Always safe to call. ## `sendChatMessage(text)` Sends a chat message. **Auto-starts a chat session** if none is active - there is no separate "connect" step. ```js theme={null} widget.sendChatMessage('Hi! What are your opening hours?'); ``` **Edge behavior:** * Called before ready → messages are **FIFO-queued** and flushed in order. * Empty or whitespace-only text → ignored. * Chat not enabled (or chat assistant missing for Retell/ElevenLabs) → fires `widget.error`. ## `endChatSession()` Ends the active chat session (server-side end plus cleanup). The next `sendChatMessage()` starts a fresh session. ```js theme={null} widget.endChatSession(); ``` **Edge behavior:** no-op when there is no session. Clears any queued messages. ## `acceptConsent()` Records the visitor's consent (persisted in `localStorage` under the widget's configured key) and **automatically resumes** the action that triggered `widget.consent-required` - the pending call starts, or the queued chat message sends. ```js theme={null} widget.addEventListener('widget.consent-required', () => { // show your own consent UI, then on accept: widget.acceptConsent(); }); ``` **Edge behavior:** no-op if consent wasn't requested. "Declining" is simply not calling it. See [Consent in Headless Mode](/documentation/widget/headless-mode/consent) for the full consent flow, including the compliance responsibilities that come with rendering your own consent UI. # Quick start Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/quickstart Go from embed snippet to your own call button in five minutes ## Before you start Headless Mode is available on our Pro tiers only. If your account is on a lower tier, upgrade your plan from the [dashboard](https://dashboard.voiceaiwrapper.app) before following this guide. You need your widget's embed snippet. In the dashboard, open your campaign and go to the **Web Widget tab → Embed Code** panel. The snippet there has your real widget ID and host pre-filled - the only thing you add is the `hide-launcher` attribute. Paste the embed snippet into your page and add the `hide-launcher` attribute. The widget mounts and runs, but renders no UI of any kind. ```html theme={null} ``` Any button, link, or image on your page can start a call or send a chat message. ```html theme={null} ``` You don't need to wait for the widget to load before calling `startCall()` - calls made early are buffered and fire automatically once the widget is ready. The widget reports everything through standard DOM events on the `` element. Use them to drive your UI state. ```js theme={null} widget.addEventListener('widget.ready', () => { btn.disabled = false; btn.textContent = 'Talk to our AI'; }); widget.addEventListener('voice.started', () => { btn.textContent = 'End call'; btn.onclick = () => widget.stopCall(); }); widget.addEventListener('voice.ended', () => { btn.textContent = 'Talk to our AI'; btn.onclick = () => widget.startCall(); }); ``` In Headless Mode your UI is the only UI - there is no widget surface to display problems. At minimum, handle these: ```js theme={null} widget.addEventListener('voice.mic-permission-denied', () => { // Tell the visitor to allow the microphone via the address-bar lock icon }); widget.addEventListener('voice.error', (e) => console.error(e.detail.message)); widget.addEventListener('chat.error', (e) => console.error(e.detail.message)); widget.addEventListener('widget.error', (e) => console.error(e.detail.message)); ``` Serve your page over `https://` (or `http://localhost` during development). Never test from a `file://` page - browsers don't persist microphone permission there, which causes repeated permission prompts and calls that die with no audio. See [Troubleshooting](/documentation/widget/headless-mode/troubleshooting) for details. ## Embed reference ### Custom element attributes ```html theme={null} ``` | Attribute | Required | Description | | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Yes | The widget ID from the dashboard (Web Widget tab). Public identifier - safe to expose in page source. | | `host` | Yes | Backend host serving the widget config and APIs (your domain or white-label domain). A bare host is auto-prefixed with `https://`. | | `hide-launcher` | No | **The Headless Mode switch.** Boolean attribute (presence = on). The widget mounts and runs but renders no UI of any kind - no launcher, no call panel, no chat window, no consent popup, not even during an active call. | **Local development only:** an additional `http-only="true"` attribute keeps a protocol-less `host` on `http://` instead of upgrading to `https://`. Never use it in production. ### Programmatic init (alternative to the custom element) If you prefer to initialize the widget from JavaScript instead of placing the custom element in your markup: ```js theme={null} const instance = window.VoiceAIWidget.init('container-element-id', { id: 'YOUR_WIDGET_ID', host: 'your-domain.com', hideLauncher: true, }); // instance has: destroy(), startCall(), stopCall(), sendChatMessage(text), // endChatSession(), acceptConsent() ``` Events are dispatched on the container element you passed to `init()`. ### Global conveniences `window.VoiceAIWidget.startCall()`, `stopCall()`, `sendChatMessage(text)`, `endChatSession()`, and `acceptConsent()` proxy to the **first mounted widget instance**. Convenient for single-widget pages; with multiple widgets, call methods on the specific element instead. ### Multiple widgets per page Supported. Each `` element is independent - methods are called on the element, events fire on the element. The globals target the first instance only. ## Using a framework? No framework bindings are needed - grab the element (with a ref or `querySelector`), call methods on it, and add event listeners. The API is plain DOM, so it works identically in React, Vue, Next.js, Svelte, and Webflow custom code. In single-page apps, removing the `` element from the DOM destroys the instance and ends active sessions. If conversations must survive client-side navigation, mount the element once at the layout level. ## Next steps Every method, including edge behavior and the readiness model. All events with payloads and ordering guarantees. # Troubleshooting & FAQ Source: https://docs.voiceaiwrapper.app/documentation/widget/headless-mode/troubleshooting Requirements, gotchas, and answers to common Headless Mode questions ## Requirements & gotchas **Never open your page via `file://`.** The page must be served over `https://` (or `http://localhost` for development). Browsers don't persist microphone permission on `file://` pages, causing repeated permission prompts and calls that die with no audio. This is the single most common integration problem. ### Microphone permission Mic access is checked via `getUserMedia` *before* any provider connection, on all providers, so you always get a consistent `voice.mic-permission-denied` event. The probe stream is released immediately. When permission is denied, guide visitors to the lock icon in the browser's address bar to re-allow the microphone. ### Audio output `hide-launcher` hides UI only - audio is unaffected. The provider SDKs attach their audio elements to `document.body`, so hiding the widget never mutes the call. ### Browser support Anything with Custom Elements and WebRTC - all evergreen browsers. Safari: microphone permission prompts may be per-session unless the visitor sets "Allow" for the site. ### Single-page apps The element works inside React, Vue, and Next.js layouts. Keep in mind: * **Removing the element from the DOM destroys the instance** and ends active sessions. For client-side routed apps, mount it once at the layout level if conversations must survive navigation. * **Page navigation or refresh ends calls and chat sessions.** Chat cleanup uses `keepalive` requests so server logs close properly. ### Error handling is your job In Headless Mode there is no widget UI to show errors. Handle at minimum: * `voice.mic-permission-denied` * `voice.error` * `chat.error` * `widget.error` ### The widget ID is public It's visible in any embedding page's source by design; the config endpoint is public. No secrets are exposed - provider private keys never reach the browser, and conversation tokens are minted per-conversation by the backend. ### Visible widget + API together The JS API also works *without* `hide-launcher` - methods and events function alongside the normal launcher. Headless Mode is just the "renders nothing" variant. ### Per-embed, not per-campaign `hide-launcher` is an attribute on the embed snippet. The same widget can be embedded normally on page A and headless on page B. ## FAQ Headless Mode is available on our Pro tiers only. You can upgrade your plan from the [dashboard](https://dashboard.voiceaiwrapper.app). No. One script tag plus one custom element. The API is plain DOM methods and events. Yes - grab the element (ref or `querySelector`), call methods, add event listeners. No framework bindings needed. All of them (Vapi, Retell, ElevenLabs) with an identical API. You don't need to know or care which provider the campaign uses. Yes - the voice and chat surfaces are independent. Yes - `hide-launcher` is per-embed. Embed the same widget normally on one page and headless on another. It's buffered and fires automatically once the widget is ready. Chat messages are queued in order the same way. Listen to `chat.agent-typing-started` / `chat.agent-typing-stopped`. The pair is always balanced, so your typing indicator can never get stuck. Either the inactivity timeout (configurable per widget, default 3 minutes) or a tab switch - check the `reason` field on `chat.session-ended`. No - it's a public identifier, like a publishable key. No - audio is independent of UI. The call sounds exactly the same with or without `hide-launcher`. ## Still stuck? [Contact support](/general/support) and include which event (or missing event) you're seeing - the `voice.*` / `chat.*` event stream usually pinpoints the issue quickly.