# API Documentation (legacy) > The AI Voice Calling Agent API provides a comprehensive platform for managing user campaigns, triggering automated outbound voice calls, and analyzing campaign performance. The system allows you to organize users into campaigns, attach relevant data, create triggers with AI-generated columns, and execute targeted calling campaigns. URL: /api-reference/call-trigger-apis Markdown: /api-reference/call-trigger-apis.md ## Base URLs [#base-urls] * **Production**: `https://api.revrag.ai` * **Staging**: `https://staging-api.revrag.ai` ## Authentication [#authentication] All API requests require an API key to be included in the request headers: ``` X-API-Key: YOUR_API_KEY ``` ## Single Trigger APIs [#single-trigger-apis] > **πŸš€ Quick Start:** Want to test these APIs immediately? Check out our [Postman Collection](./postman-collection) with all endpoints pre-configured and ready to import! ### 1. Get All Agents [#1-get-all-agents] **GET** `/v1/campaigns/agents` Get All Agents #### Header Parameters [#header-parameters] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `X-API-Key` | string | Yes | API Key for authentication | #### Response [#response] **Success (200 OK)** ```json [ { "id": "agent_123e4567-e89b-12d3-a456-426614174000", "name": "customer-support-agent", "variables": [ { "name": "customer_name", "type": "string", "default_value": null } ] } ] ``` #### cURL Example [#curl-example] ```bash curl -X GET "https://staging-api.revrag.ai/v1/campaigns/agents" \ -H "X-API-Key: YOUR_API_KEY" ``` *** ### 2. Trigger Single Call [#2-trigger-single-call] **POST** `/v1/campaigns/trigger/single` Trigger a single AI call for testing purposes. #### Header Parameters [#header-parameters-1] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `X-API-Key` | string | Yes | API Key for authentication | #### Request Body [#request-body] ```json { "agent_id": "agent_123e4567-e89b-12d3-a456-426614174000", "to_phone_number": "+1234567890", "variable_fields": { "first_name": "John", "company": "Acme Inc." }, "custom_metadata": { "campaign_type": "follow_up", "source": "web_form", "priority": "high" } } ``` #### Request Parameters [#request-parameters] | Parameter | Type | Required | Description | | ----------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `agent_id` | string (uuid) | Yes | The ID of the agent | | `to_phone_number` | string | Yes | Recipient phone number | | `variable_fields` | object or null | No | Variable fields for the call | | `custom_metadata` | object or null | No | Custom metadata fields for tracking and organization. This field is flexible and can accept any key-value pairs | #### Response [#response-1] **Success (200 OK)** ```json { "success": true, "message": "Call triggered successfully", "to_phone_number": "+1234567890", "agent_id": "agent_123e4567-e89b-12d3-a456-426614174000", "call_id": "call_456e7890-f12c-34d5-b678-901234567890" } ``` #### cURL Example [#curl-example-1] ```bash curl -X POST "https://staging-api.revrag.ai/v1/campaigns/trigger/single" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "agent_123e4567-e89b-12d3-a456-426614174000", "to_phone_number": "+1234567890", "variable_fields": { "first_name": "John", "company": "Acme Inc." }, "custom_metadata": { "campaign_type": "follow_up", "source": "web_form", "priority": "high" } }' ``` **Validation Error (422)** ```json { "detail": [ { "loc": ["body", "to_phone_number"], "msg": "Invalid phone number format", "type": "value_error" } ] } ``` *** ### 3. Get Call Status [#3-get-call-status] **GET** `/v1/campaigns/trigger/status/{call_id}` Get the status of a specific call by its call\_id. #### Path Parameters [#path-parameters] | Parameter | Type | Required | Description | | --------- | ------------- | -------- | ------------------ | | `call_id` | string (uuid) | Yes | The ID of the call | #### Header Parameters [#header-parameters-2] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `X-API-Key` | string | Yes | API Key for authentication | #### Response [#response-2] **Success (200 OK)** ```json { "call_status": "ENDED", "call_id": "call_123e4567-e89b-12d3-a456-426614174000", "agent_id": "agent_456e7890-f12c-34d5-b678-901234567890", "from_number": "+918045342561", "to_number": "+1234567890", "summary": "Customer inquired about pricing options for our services. Discussed various pricing tiers and provided information about premium features. Customer showed interest in upgrading their current plan.", "start_time": "2024-01-15T10:00:00.000000", "end_time": "2024-01-15T10:15:30.500000", "duration": 930.5, "disconnection_reason": "USER_HANGUP", "recording_url": "https://cdn.revrag.ai/voice/recordings/room_abc123def456/room_abc123def456_audio.mp4", "transcription": { "messages": [ { "role": "assistant", "content": "Hello, this is Sarah from Customer Support. Am I speaking with John?", "timestamp": "2024-01-15T10:00:15.000000+00:00" }, { "role": "user", "content": "Yes, this is John speaking.", "timestamp": "2024-01-15T10:00:25.000000+00:00" }, { "role": "assistant", "content": "Great! I'm calling to follow up on your recent inquiry about our services.", "timestamp": "2024-01-15T10:00:35.000000+00:00" }, { "role": "user", "content": "Yes, I was interested in learning more about the pricing options.", "timestamp": "2024-01-15T10:00:50.000000+00:00" } ] }, "custom_variables": [ { "key": "customer_tier", "description": "Customer subscription tier level", "value": "premium", "type": "string" }, { "key": "account_balance", "description": "Current account balance in USD", "value": 1250.75, "type": "number" }, { "key": "is_first_time_caller", "description": "Whether this is the customer's first call", "value": false, "type": "boolean" } ], "variables_fields": { "first_name": "John", "company": "Acme Inc." }, "custom_metadata": { "campaign_type": "follow_up", "source": "web_form", "priority": "high" } } ``` #### Response Parameters [#response-parameters] | Parameter | Type | Description | | ---------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `call_status` | string | Current status of the call (QUEUED, RUNNING, or ENDED) | | `call_id` | string (uuid) | Unique identifier for the call | | `agent_id` | string (uuid) | ID of the agent used for this call | | `from_number` | string | Phone number from which the call was made | | `to_number` | string | Phone number to which the call was made | | `summary` | string | AI-generated summary of the call conversation | | `start_time` | string | ISO 8601 timestamp when the call started | | `end_time` | string | ISO 8601 timestamp when the call ended | | `duration` | number | Call duration in seconds | | `disconnection_reason` | string | Reason why the call ended (see Disconnection Reason Values) | | `recording_url` | string | URL to the call recording (MP4 format) | | `transcription` | object | Full transcription of the call with messages and timestamps | | `custom_variables` | array | AI-extracted variables from the conversation during the call | | `variables_fields` | object or null | The original variable fields that were passed when triggering the call (note: the request body uses `variable_fields`; the response key has an extra `s`) | | `custom_metadata` | object or null | The original custom metadata that was passed when triggering the call | #### cURL Example [#curl-example-2] ```bash curl -X GET "https://staging-api.revrag.ai/v1/campaigns/trigger/status/call_123e4567-e89b-12d3-a456-426614174000" \ -H "X-API-Key: YOUR_API_KEY" ``` *** ### 4. Webhook Notifications (Optional) [#4-webhook-notifications-optional] Instead of polling the call status endpoint, you can configure a webhook URL to receive real-time call completion notifications. When a call ends, we'll automatically send the same response body structure as the "Get Call Status" API to your webhook endpoint. #### Webhook Configuration [#webhook-configuration] Contact our support team at **[contact@revrag.ai](mailto:contact@revrag.ai)** to configure your webhook URL. #### Webhook Payload [#webhook-payload] The webhook will receive a **POST** request with the same JSON structure as the call status response, including the original values you passed as `variable_fields` (echoed back on the payload as `variables_fields`) and `custom_metadata`: ```json { "call_status": "ENDED", "call_id": "call_123e4567-e89b-12d3-a456-426614174000", "agent_id": "agent_456e7890-f12c-34d5-b678-901234567890", "from_number": "+918045342561", "to_number": "+1234567890", "summary": "Customer inquired about pricing options for our services. Discussed various pricing tiers and provided information about premium features. Customer showed interest in upgrading their current plan.", "start_time": "2024-01-15T10:00:00.000000", "end_time": "2024-01-15T10:15:30.500000", "duration": 930.5, "disconnection_reason": "USER_HANGUP", "recording_url": "https://cdn.revrag.ai/voice/recordings/room_abc123def456/room_abc123def456_audio.mp4", "transcription": { "messages": [ { "role": "assistant", "content": "Hello, this is Sarah from Customer Support. Am I speaking with John?", "timestamp": "2024-01-15T10:00:15.000000+00:00" }, { "role": "user", "content": "Yes, this is John speaking.", "timestamp": "2024-01-15T10:00:25.000000+00:00" } ] }, "custom_variables": [ { "key": "customer_tier", "description": "Customer subscription tier level", "value": "premium", "type": "string" }, { "key": "account_balance", "description": "Current account balance in USD", "value": 1250.75, "type": "number" }, { "key": "is_first_time_caller", "description": "Whether this is the customer's first call", "value": false, "type": "boolean" } ], "variables_fields": { "first_name": "John", "company": "Acme Inc." }, "custom_metadata": { "campaign_type": "follow_up", "source": "web_form", "priority": "high" } } ``` *** ### Call Status Values [#call-status-values] | Status | Description | | --------- | ----------------------------- | | `QUEUED` | Call is queued for execution | | `RUNNING` | Call is currently in progress | | `ENDED` | Call has ended | ### Disconnection Reason Values [#disconnection-reason-values] When a call ends, the `disconnection_reason` field indicates why the call was terminated: | Reason | Description | | -------------------- | -------------------------------------------------------------------------------- | | `USER_HANGUP` | The callee hung up the call | | `USER_REJECTED` | Callee rejected the call (busy) | | `MAX_DURATION` | The set maximum call duration was reached | | `VOICEMAIL_REACHED` | Voicemail was reached | | `AGENT_HANGUP` | The agent ended the call as per prompt or upon detecting conversation completion | | `SILENCE_TIMEOUT` | Callee was silent for longer than the configured silence timeout period | | `CALL_NOT_PICKED` | Call was not picked up by the callee | | `ERROR_OCCURRED` | An error occurred during the call | | `CALL_NOT_CONNECTED` | Call was not connected due to Telephony Issue | | `CALL_DROPPED` | Call was dropped in Between due to Network Issues | *** ## Support [#support] If you encounter any issues or need assistance with the API, please contact our support team: **Email**: [contact@revrag.ai](mailto:contact@revrag.ai) Our team will respond to your inquiries and help resolve any technical issues you may experience while using the API. --- # Campaigns API > Push one or more records to a RevRag campaign via the API and let the agent dial them per the campaign's schedule, retry, and concurrency configuration; then poll record and campaign status, or receive webhooks. URL: /api-reference/campaigns-api Markdown: /api-reference/campaigns-api.md ## Overview [#overview] A **campaign** in RevRag is a workspace-scoped definition that bundles an agent, a schedule, retry/follow-up rules, and concurrency limits. This page documents the endpoints you use to push records into an already-running campaign and to track their status: | Endpoint | Use when | | ------------------------- | ------------------------------------------------------------------------------------- | | `POST /records/push-bulk` | Push one or more records into the campaign (a single record goes as a 1-element list) | | `GET /records/{run_id}` | Poll the status of a record you pushed (by the `run_id` returned on push) | | `GET /records` | List a campaign's records, filterable by status | | `GET /status` | Get the campaign's status + per-status record counts | The push endpoint returns a **`run_id`** for every record so you can correlate and poll each one. The campaign itself β€” name, agent, schedule, end date, retry rules, concurrency β€” is configured and launched in the RevRag dashboard. By the time you call these APIs, the campaign should already be running. The dashboard's **Campaign β†’ Settings** tab shows a copy-pasteable curl with your campaign's id pre-filled. ## Base URLs [#base-urls] * **Production**: `https://api.revrag.ai` * **Staging**: `https://staging-api.revrag.ai` ## Authentication [#authentication] All requests require a workspace-scoped API key in the `X-API-Key` header: ``` X-API-Key: YOUR_API_KEY ``` The workspace is resolved from the API key itself β€” you do not pass a workspace header. The campaign in the path must belong to the same workspace as the API key, otherwise the request returns `404 Not Found`. ## Before You Push [#before-you-push] The campaign is fully created, configured, and started from the RevRag dashboard. From the API caller's side you only need: * The **`campaign_id`** β€” copy it from the dashboard's Campaign β†’ Settings tab (it's also in the URL of the campaign detail page). * Your **`X-API-Key`** β€” issued for your workspace. * The agent's **variable names**, if the agent expects per-call variables. Variable names are visible on the campaign's Settings tab. ## Endpoints [#endpoints] ### 1. Push Records [#1-push-records] **POST** `/v1/campaigns/{campaign_id}/records/push-bulk` Queue one or more records for calling. Send a single record as a **1-element `records` array**. Each record is treated independently β€” the runner picks them up on its next polling cycle (typically within \~1 second) and dispatches each call subject to the campaign's daily time-window, attempt schedule, and concurrency cap. Invalid records are rejected individually (partial success), all in a single `200 OK`. #### Path Parameters [#path-parameters] | Parameter | Type | Required | Description | | ------------- | ------------- | -------- | --------------------------------- | | `campaign_id` | string (uuid) | Yes | The campaign to push records into | #### Header Parameters [#header-parameters] | Parameter | Type | Required | Description | | -------------- | ------ | -------- | -------------------------- | | `X-API-Key` | string | Yes | Workspace-scoped API key | | `Content-Type` | string | Yes | Must be `application/json` | #### Request Body [#request-body] ```json { "records": [ { "phone_number": "+919876543210", "variables": { "first_name": "Pankaj", "company": "Acme Inc." }, "app_user_id": "user_12345", "app_session_id": "sess_abcdef" }, { "phone_number": "+919876543211", "variables": { "first_name": "Riya" } } ] } ``` #### Request Parameters [#request-parameters] | Parameter | Type | Required | Description | | --------- | ----- | -------- | ----------------------------------------------------------------------- | | `records` | array | Yes | One or more records to push (use a 1-element array for a single record) | Each record in `records`: | Field | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `phone_number` | string | Yes | E.164 format recommended (`+919876543210`) | | `variables` | object | No | Key-value pairs matching the agent's variable names. Sent into the agent's prompt at call time | | `app_user_id` | string | No | Your system's user id β€” a **top-level** correlation field (not inside `variables`). Returned top-level on the record and in webhooks | | `app_session_id` | string | No | Your system's session id β€” a **top-level** correlation field (not inside `variables`). Returned top-level on the record and in webhooks | **Cross-agent memory:** send `app_user_id` if you want this call to share memory with the same user's in-app agent sessions. RevRag keys conversation memory on `app_user_id`, so the calling agent picks up context from the in-app agent (and vice versa) only when both send the **same** id. Use the same value you pass in the SDK's `USER_DATA` event. Omit it and the call starts with no prior context. #### Response [#response] The response returns a **per-record result** for every record you submitted, in the same order as the `records` array (`results[i]` corresponds to `records[i]`). Each accepted record carries its own **`run_id`** so you can correlate every record to its created run and poll its status later (see [Get Record Status](#2-get-record-status)). **Success (200 OK)** ```json { "accepted": 2, "rejected": 0, "results": [ { "phone_number": "+919876543210", "accepted": true, "run_id": "00000000-0000-0000-0000-000000000001", "status": "queued", "error": null }, { "phone_number": "+919876543211", "accepted": true, "run_id": "00000000-0000-0000-0000-000000000002", "status": "queued", "error": null } ] } ``` **Partial success (200 OK)** Invalid records are rejected individually β€” the valid ones are still created. A rejected record has `accepted: false`, `run_id: null`, and an `error` message. ```json { "accepted": 1, "rejected": 1, "results": [ { "phone_number": "+919876543210", "accepted": true, "run_id": "00000000-0000-0000-0000-000000000001", "status": "queued", "error": null }, { "phone_number": "not-a-number", "accepted": false, "run_id": null, "status": null, "error": "Invalid Indian phone number: not-a-number" } ] } ``` #### Response Parameters [#response-parameters] | Parameter | Type | Description | | ---------- | ------ | ---------------------------------------------------------------------------------- | | `accepted` | number | How many records were inserted and queued for calling | | `rejected` | number | How many were rejected (e.g. invalid phone number) | | `results` | array | Per-record outcome, in the same order as the submitted `records`. See fields below | Each entry in `results`: | Field | Type | Description | | -------------- | --------------------- | --------------------------------------------------------------------------------------------------------- | | `phone_number` | string | The phone number you submitted for this record | | `accepted` | boolean | `true` if the record was created and queued, `false` if rejected | | `run_id` | string (uuid) \| null | The created run id β€” present when `accepted: true`. Use it with [Get Record Status](#2-get-record-status) | | `status` | string \| null | Initial status (`queued`) when accepted, `null` when rejected | | `error` | string \| null | Why the record was rejected β€” present when `accepted: false` | > Pushing a single record? Send a 1-element `records` array β€” you get that record's `run_id` back in `results[0]`. #### cURL Example [#curl-example] ```bash curl -X POST \ "https://staging-api.revrag.ai/v1/campaigns/{campaign_id}/records/push-bulk" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "phone_number": "+919876543210", "variables": { "first_name": "Pankaj" } }, { "phone_number": "+919876543211", "variables": { "first_name": "Riya" } } ] }' ``` *** ### 2. Get Record Status [#2-get-record-status] **GET** `/v1/campaigns/{campaign_id}/records/{run_id}` Fetch the current status and details of a pushed record, using the `run_id` you got back from the push (`results[].run_id`). #### Path Parameters [#path-parameters-1] | Parameter | Type | Required | Description | | ------------- | ------------- | -------- | -------------------------------------------- | | `campaign_id` | string (uuid) | Yes | The campaign the record belongs to | | `run_id` | string (uuid) | Yes | The record's run id (from the push response) | #### Header Parameters [#header-parameters-1] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------ | | `X-API-Key` | string | Yes | Workspace-scoped API key | #### Response [#response-1] **Success (200 OK)** β€” the full campaign record: ```json { "id": "00000000-0000-0000-0000-000000000001", "campaign_id": "11111111-1111-1111-1111-111111111111", "workspace_id": "22222222-2222-2222-2222-222222222222", "phone_number": "+919876543210", "status": "completed", "variables": { "first_name": "Pankaj" }, "app_user_id": "user_12345", "app_session_id": "sess_abcdef", "attempt_count": 1, "max_attempts": 3, "call_history": [{ "call_id": "...", "status": "completed" }], "next_attempt_at": null, "dnc_at": null, "dnc_reason": null, "created_at": "2026-05-27T03:00:00.000000+00:00", "updated_at": "2026-05-27T03:05:12.000000+00:00" } ``` See [Record Status Values](#record-status-values) for what each `status` means. The `call_history` array holds one entry per attempt, including the per-call `call_id` you can pass to [Get Call Status](#get-call-status) for the transcript and recording. #### cURL Example [#curl-example-1] ```bash curl -X GET \ "https://staging-api.revrag.ai/v1/campaigns/{campaign_id}/records/{run_id}" \ -H "X-API-Key: YOUR_API_KEY" ``` *** ### 3. List Records [#3-list-records] **GET** `/v1/campaigns/{campaign_id}/records` List the campaign's records, optionally filtered by status β€” useful for polling everything in a given state (e.g. all `queued`, or all `completed`). #### Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `status` | string | No | Filter by [record status](#record-status-values) (e.g. `queued`, `completed`, `failed`) | | `page` | number | No | 1-based page number (default `1`) | | `page_size` | number | No | Records per page (default `50`, max `200`) | #### Response [#response-2] **Success (200 OK)** ```json { "items": [ { "id": "00000000-0000-0000-0000-000000000001", "phone_number": "+919876543210", "status": "completed", "attempt_count": 1, "max_attempts": 3 } ], "total": 100, "has_more": true, "page": 1, "page_size": 50 } ``` Each item has the same fields as a single record (see [Get Record Status](#2-get-record-status)). #### cURL Example [#curl-example-2] ```bash curl -X GET \ "https://staging-api.revrag.ai/v1/campaigns/{campaign_id}/records?status=completed&page=1&page_size=50" \ -H "X-API-Key: YOUR_API_KEY" ``` *** ### 4. Get Campaign Status [#4-get-campaign-status] **GET** `/v1/campaigns/{campaign_id}/status` Get the campaign's lifecycle status plus aggregate per-status record counts β€” a quick way to track overall progress without listing every record. #### Response [#response-3] **Success (200 OK)** ```json { "campaign_id": "11111111-1111-1111-1111-111111111111", "status": "running", "stats": { "total_records": 100, "queued": 18, "in_progress": 4, "completed": 70, "failed": 3, "dnc": 2, "rescheduled": 1, "retry_pending": 2, "follow_up_pending": 0 } } ``` #### Response Parameters [#response-parameters-1] | Parameter | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `campaign_id` | string (uuid) | The campaign | | `status` | string | Campaign lifecycle status (`running`, `paused`, `completed`, `cancelled`, …) | | `stats` | object | Per-status record counts (`total_records`, `queued`, `in_progress`, `completed`, `failed`, `dnc`, `rescheduled`, `retry_pending`, `follow_up_pending`) | #### cURL Example [#curl-example-3] ```bash curl -X GET \ "https://staging-api.revrag.ai/v1/campaigns/{campaign_id}/status" \ -H "X-API-Key: YOUR_API_KEY" ``` *** ## How Records are Called [#how-records-are-called] Pushing a record does not always trigger an immediate dial. After your push is accepted, the campaign runner picks up queued records each cycle and applies the campaign's configured gates (all set in the dashboard): | Gate | Effect on your record | | -------------------------------------------------------------------------- | ----------------------------------------- | | Outside the campaign's daily calling window | Stays queued until the window opens | | Day not in the campaign's active days (e.g. weekend on a Mon-Fri campaign) | Queued until the next active day's window | | First-attempt schedule is later than today | Queued until that day | | Campaign's max concurrency reached | Queued until a slot frees | | Phone is in the workspace's Do-Not-Call list | Marked `dnc`, no call placed | When all gates are open, dispatch is typically within \~1 second of the push. See [Record Status Values](#record-status-values) for what each status means in flight. If the campaign has **Dial first attempt immediately** enabled (a dashboard setting), the **first** attempt skips the *first-attempt schedule* gate and is dialed as soon as the record is pushed β€” but still only within the calling window and the concurrency/DNC gates. Retries and follow-ups (attempts 2+) follow all the gates above as normal. If you push to a campaign that has finished (past its end date) or has been cancelled/paused-and-stopped from the dashboard, the API returns `400`. The campaign must be in an active state on the dashboard to accept pushes. ## Get Call Status [#get-call-status] **GET** `/v1/campaigns/trigger/status/{call_id}` Fetch the status, transcript, recording URL, summary, and extracted variables for a specific call. The `call_id` for a pushed record is available in the record's `call_history` (via [Get Record Status](#2-get-record-status)), delivered via webhook (see below), or visible on the campaign's Records tab in the dashboard. The push response itself returns the record's **`run_id`** (not the per-call `call_id`) β€” the call is placed asynchronously by the campaign runner, so poll the record (or wait for the webhook) to get the `call_id`. #### Path Parameters [#path-parameters-2] | Parameter | Type | Required | Description | | --------- | ------------- | -------- | -------------------------------------------------------------------- | | `call_id` | string (uuid) | Yes | The call id, obtained from the webhook payload or from the dashboard | #### Header Parameters [#header-parameters-2] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------ | | `X-API-Key` | string | Yes | Workspace-scoped API key | #### Response [#response-4] **Success (200 OK)** ```json { "call_status": "completed", "call_id": "123e4567-e89b-12d3-a456-426614174000", "agent_id": "33333333-3333-3333-3333-333333333333", "from_number": "+911140000000", "to_number": "+919876543210", "summary": "Customer confirmed the appointment for Friday.", "start_time": "2026-05-27T03:05:00+00:00", "end_time": "2026-05-27T03:06:12+00:00", "duration": 72, "disconnection_reason": "agent_hangup", "recording_url": "https://.../recording.mp3", "transcription": { "messages": [ { "role": "assistant", "content": "Hi Pankaj, calling about your appointment…" }, { "role": "user", "content": "Yes, Friday works." } ] }, "custom_variables": [ { "key": "appointment_confirmed", "type": "boolean", "value": true } ], "variables_fields": { "first_name": "Pankaj" }, "campaign_id": "11111111-1111-1111-1111-111111111111", "attempt_no": 1, "app_user_id": "user_12345", "app_session_id": "sess_abcdef" } ``` #### Response Parameters [#response-parameters-2] | Field | Type | Description | | -------------------------------- | --------------------- | --------------------------------------------------------------------------------- | | `call_status` | string | Final call status (e.g. `completed`, `failed`) | | `call_id` | string (uuid) | The call this status is for | | `agent_id` | string (uuid) | The agent that placed the call | | `from_number` / `to_number` | string | Caller / callee numbers | | `summary` | string | LLM-generated call summary (when a transcript exists) | | `start_time` / `end_time` | string \| null | ISO-8601 timestamps | | `duration` | number | Call duration in seconds | | `disconnection_reason` | string \| null | Normalized hangup reason | | `recording_url` | string \| null | Presigned recording URL (for transcribed calls) | | `transcription` | object \| null | Turn-by-turn messages | | `custom_variables` | array | Post-call variables extracted by the agent (`key`, `type`, `value`) | | `variables_fields` | object \| null | The `variables` you sent on push (correlation IDs are surfaced top-level instead) | | `campaign_id` | string (uuid) \| null | Campaign the call belongs to (null for non-campaign calls) | | `attempt_no` | number \| null | Which attempt this call was | | `app_user_id` / `app_session_id` | string \| null | Your correlation IDs (top-level), if you sent them | #### cURL Example [#curl-example-4] ```bash curl -X GET \ "https://staging-api.revrag.ai/v1/campaigns/trigger/status/123e4567-e89b-12d3-a456-426614174000" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Webhook Notifications [#webhook-notifications] Instead of polling for status, configure a webhook URL to receive a `POST` notification when each call ends. Webhooks fire for every call placed by the agent β€” including calls dispatched from campaigns pushed via this API. #### Configuration [#configuration] Contact **[contact@revrag.ai](mailto:contact@revrag.ai)** to configure your webhook URL and signing secret. #### Security & Headers [#security--headers] Campaign webhooks use the **same** signing scheme as every RevRag webhook β€” HMAC-SHA256 computed over `"{timestamp}.{raw_body}"` with your shared secret. Each request carries: | Header | Purpose | | --------------------- | --------------------------------------------- | | `X-Webhook-Event` | Event type (`call.ended`) | | `X-Webhook-Timestamp` | Unix timestamp (seconds) the webhook was sent | | `X-Webhook-Signature` | `t=,v1=` | | `X-Webhook-ID` | Unique id for deduplication | See [Webhook Security](./webhook-security) for the full verification steps and Python / Node.js examples. #### Payload [#payload] The webhook body is the same JSON shape as the [Get Call Status](#get-call-status) response. For calls placed from a campaign it also carries **`campaign_id`**, **`attempt_no`**, and your top-level correlation IDs **`app_user_id`** / **`app_session_id`**. The `variables` you sent are echoed back as **`variables_fields`** (with the correlation IDs kept out of it). ```json { "call_status": "completed", "call_id": "123e4567-e89b-12d3-a456-426614174000", "agent_id": "33333333-3333-3333-3333-333333333333", "from_number": "+911140000000", "to_number": "+919876543210", "summary": "Customer confirmed the appointment for Friday.", "start_time": "2026-05-27T03:05:00+00:00", "end_time": "2026-05-27T03:06:12+00:00", "duration": 72, "disconnection_reason": "agent_hangup", "recording_url": "https://.../recording.mp3", "transcription": { "messages": [{ "role": "assistant", "content": "Hello…" }] }, "custom_variables": [ { "key": "appointment_confirmed", "type": "boolean", "value": true } ], "variables_fields": { "first_name": "Pankaj" }, "campaign_id": "11111111-1111-1111-1111-111111111111", "attempt_no": 1, "app_user_id": "user_12345", "app_session_id": "sess_abcdef" } ``` ## Record Status Values [#record-status-values] | Status | Description | | ------------------- | ---------------------------------------------------------------------------------------------------- | | `queued` | Accepted, waiting for the runner to pick it up | | `in_progress` | Call is being placed or is live | | `completed` | Call finished β€” see `call_history` on the record for details | | `failed` | Call attempt failed (e.g. telephony error). Will retry if attempts remain and retry conditions match | | `retry_pending` | Waiting for the next retry attempt at `next_attempt_at` | | `follow_up_pending` | Waiting for a follow-up attempt scheduled by post-call logic | | `rescheduled` | Caller requested a callback β€” will dispatch at the requested time | | `dnc` | Phone added to Do-Not-Call list, no further attempts | ## Common Errors [#common-errors] | Status | When | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400 Bad Request` | Campaign is not currently accepting pushes (e.g. already finished or cancelled on the dashboard). A malformed `phone_number` is **not** a request-level error β€” it's rejected per-record in `results` (still `200`) | | `401 Unauthorized` | Missing or invalid `X-API-Key` header | | `404 Not Found` | `campaign_id` doesn't exist in the API key's workspace, or `campaign_id` isn't a valid UUID | | `422 Unprocessable Entity` | Request body doesn't match the schema (missing `phone_number`, malformed JSON, etc.) | **Example error body** ```json { "detail": "Cannot push records: campaign is completed." } ``` ## Support [#support] For API keys, campaign setup, or any technical issue, contact **[contact@revrag.ai](mailto:contact@revrag.ai)**. --- # Postman Collection > Ready-to-use Postman collection for testing RevRag AI Voice Calling Agent APIs. Import and start testing all Single Trigger APIs instantly. URL: /api-reference/postman-collection Markdown: /api-reference/postman-collection.md # Postman Collection [#postman-collection] **πŸš€ Ready-to-use Postman Collection for all Single Trigger APIs** We've created a complete Postman collection with all 3 Single Trigger APIs pre-configured and ready to test. This collection includes proper authentication, sample request bodies, and environment variables for seamless testing. ## **πŸ“₯ Download Options** [#-download-options] ### **Option 1: Direct Download** [#option-1-direct-download] Right-click and save: **[postman-collection.json](./postman-collection.json)** ### **Option 2: Copy & Save** [#option-2-copy--save] Copy the JSON collection below and save it as `RevRag_Single_Trigger_APIs.postman_collection.json`: ```json { "info": { "name": "RevRag Single Trigger APIs", "description": "Collection for RevRag AI Voice Calling Agent APIs - Single Trigger endpoints", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ { "key": "base_url", "value": "https://staging-api.revrag.ai", "type": "string" }, { "key": "api_key", "value": "YOUR_API_KEY", "type": "string" } ], "item": [ { "name": "1. Get All Agents", "request": { "method": "GET", "header": [ { "key": "X-API-Key", "value": "{{api_key}}", "type": "text" } ], "url": { "raw": "{{base_url}}/v1/campaigns/agents", "host": ["{{base_url}}"], "path": ["v1", "campaigns", "agents"] }, "description": "Retrieve all workspace agents with their detailed information including variables" } }, { "name": "2. Trigger Single Call", "request": { "method": "POST", "header": [ { "key": "X-API-Key", "value": "{{api_key}}", "type": "text" }, { "key": "Content-Type", "value": "application/json", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"agent_id\": \"agent_123e4567-e89b-12d3-a456-426614174000\",\n \"to_phone_number\": \"+1234567890\",\n \"variable_fields\": {\n \"first_name\": \"John\",\n \"company\": \"Acme Inc.\"\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{base_url}}/v1/campaigns/trigger/single", "host": ["{{base_url}}"], "path": ["v1", "campaigns", "trigger", "single"] }, "description": "Trigger a single AI call for testing purposes" } }, { "name": "3. Get Call Status", "request": { "method": "GET", "header": [ { "key": "X-API-Key", "value": "{{api_key}}", "type": "text" } ], "url": { "raw": "{{base_url}}/v1/campaigns/trigger/status/{{call_id}}", "host": ["{{base_url}}"], "path": ["v1", "campaigns", "trigger", "status", "{{call_id}}"], "variable": [ { "key": "call_id", "value": "call_123e4567-e89b-12d3-a456-426614174000", "description": "The ID of the call to get status for" } ] }, "description": "Get the status of a specific call by its call_id" } } ] } ``` ## **πŸ“– How to Import to Postman** [#-how-to-import-to-postman] ### **Step-by-Step Instructions:** [#step-by-step-instructions] 1. **Download or copy** the collection JSON above 2. **Open Postman** application on your computer 3. **Click "Import"** button (located in the top left corner) 4. **Select the JSON file** you downloaded, or paste the JSON content directly 5. **Click "Import"** to add the collection to your workspace 6. **Update API Key**: * Go to the collection variables tab * Replace `YOUR_API_KEY` with your actual RevRag API key 7. **Select environment**: Make sure the staging environment variables are active 8. **Start testing** all 3 APIs instantly! πŸŽ‰ ### **Environment Setup:** [#environment-setup] The collection includes these environment variables: * `{{base_url}}`: Set to `https://staging-api.revrag.ai` for staging environment * `{{api_key}}`: Replace with your actual API key * `{{call_id}}`: Used for testing the Get Call Status endpoint ## **βœ… What's Included** [#-whats-included] * βœ… **All 3 Single Trigger APIs** pre-configured with proper endpoints * βœ… **Environment variables** for easy base URL and API key management * βœ… **Sample request bodies** with realistic test data and proper formatting * βœ… **Proper headers** including `X-API-Key` authentication and content types * βœ… **Path variables** configured for dynamic call ID usage * βœ… **Descriptions** for each API endpoint explaining their purpose * βœ… **Ready for staging environment** pointing to `staging-api.revrag.ai` ## **🎯 API Endpoints Included** [#-api-endpoints-included] ### **1. Get All Agents** [#1-get-all-agents] * **Method**: GET * **Endpoint**: `/v1/campaigns/agents` * **Purpose**: Retrieve all workspace agents with variables * **Headers**: X-API-Key authentication ### **2. Trigger Single Call** [#2-trigger-single-call] * **Method**: POST * **Endpoint**: `/v1/campaigns/trigger/single` * **Purpose**: Trigger a single AI call for testing * **Headers**: X-API-Key authentication, Content-Type: application/json * **Body**: JSON with agent\_id, phone number, and variable fields ### **3. Get Call Status** [#3-get-call-status] * **Method**: GET * **Endpoint**: `/v1/campaigns/trigger/status/{call_id}` * **Purpose**: Get detailed status of a specific call * **Headers**: X-API-Key authentication * **Variables**: Dynamic call\_id from previous responses ## **πŸ’‘ Testing Workflow** [#-testing-workflow] ### **Recommended Testing Order:** [#recommended-testing-order] 1. **Start with "Get All Agents"** * Verify your API key works * Get available agent IDs for testing * Copy an agent ID for the next step 2. **Use "Trigger Single Call"** * Update the `agent_id` with a real agent ID from step 1 * Modify phone number if needed (for testing) * Execute the call and save the returned `call_id` 3. **Check "Get Call Status"** * Replace the `call_id` variable with the actual ID from step 2 * Monitor the call status and get detailed information ### **Pro Tips:** [#pro-tips] * **Save responses**: Copy important values like `call_id` from responses for use in subsequent requests * **Environment switching**: Create separate environments for staging vs production testing * **Variables usage**: Leverage Postman variables to automatically pass data between requests * **Collection runner**: Use Postman's collection runner to test all APIs in sequence * **Test scripts**: Add test scripts to automatically validate responses and extract values ## **πŸ”§ Troubleshooting** [#-troubleshooting] ### **Common Issues:** [#common-issues] **❌ 401 Unauthorized** * Check that your API key is correctly set in the collection variables * Ensure the API key has proper permissions for the endpoints **❌ 404 Not Found** * Verify the base URL is correct (`https://staging-api.revrag.ai`) * Check that endpoint paths match the API documentation **❌ 422 Validation Error** * Ensure request body format matches the expected schema * Validate phone number format and agent ID values ### **Getting Help:** [#getting-help] If you encounter issues with the Postman collection: 1. Check the [API Documentation](./call-trigger-apis) for detailed endpoint specifications 2. Verify your API key permissions and validity 3. Contact support at **[contact@revrag.ai](mailto:contact@revrag.ai)** for technical assistance *** **Ready to start testing?** Download the collection and import it to Postman to begin exploring the RevRag AI Voice Calling APIs! πŸš€ --- # Webhook Security > Learn how to securely receive and verify webhook notifications from RevRag.ai URL: /api-reference/webhook-security Markdown: /api-reference/webhook-security.md ## Overview [#overview] RevRag.ai sends webhook notifications to your configured endpoint when specific events occur (such as when a call ends). To ensure the security and authenticity of these webhooks, we implement cryptographic signature verification that allows you to confirm that requests are genuinely from RevRag.ai. ## Authentication Method [#authentication-method] We use **HMAC-based authentication** to secure webhook notifications. This method uses a shared secret to cryptographically sign webhook payloads, providing strong security guarantees for verifying the authenticity and integrity of each request. ## Configuration [#configuration] To set up webhooks, you'll need to configure: * **Webhook URL**: The HTTPS endpoint where you'll receive webhook notifications (HTTP is not supported for security reasons) * **Secret Key**: A secure shared secret used for HMAC signature verification * **Event Types**: Select which events you want to receive (currently supported: `call.ended`) Contact our support team at **[contact@revrag.ai](mailto:contact@revrag.ai)** to configure your webhook settings. ## Webhook Headers [#webhook-headers] Every webhook request includes the following headers: | Header | Purpose | Example | | --------------------- | ----------------------------------------------- | --------------------------- | | `Content-Type` | Ensures receiver parses payload correctly | `application/json` | | `X-Webhook-Event` | Identifies the event type | `call.ended` | | `X-Webhook-ID` | Unique identifier for deduplication and logging | `evt_01HC3Q0MZQABR3...` | | `X-Webhook-Timestamp` | Unix timestamp when the webhook was sent | `1698064496` | | `X-Webhook-Signature` | HMAC signature for verifying authenticity | `t=1698064496,v1=abc123...` | ## Webhook Payload [#webhook-payload] The webhook payload contains the same data structure as described in the [Webhook Notifications section](/api-reference/call-trigger-apis#4-webhook-notifications-optional) of the API documentation. For complete payload details and field descriptions, please refer to that section. ## Signature Verification [#signature-verification] ### How It Works [#how-it-works] We use HMAC-SHA256 to sign every webhook request. The signature is computed over a string consisting of: ``` timestamp + "." + raw_request_body ``` Where: * `timestamp` is the Unix timestamp (in seconds) from the `X-Webhook-Timestamp` header * `raw_request_body` is the exact byte sequence of the JSON payload (before parsing) ### Signature Format [#signature-format] The `X-Webhook-Signature` header contains two components: ``` t=,v1= ``` * `t`: The timestamp when the signature was generated * `v1`: The HMAC-SHA256 signature in hexadecimal format ### Verification Steps [#verification-steps] To verify a webhook request is authentic, follow these steps: 1. **Extract the timestamp and signature** from the `X-Webhook-Signature` header 2. **Verify the timestamp** is within an acceptable time window (recommended: Β±5 minutes) to prevent replay attacks 3. **Reconstruct the signed string** by concatenating the timestamp, a period, and the raw request body 4. **Compute the HMAC-SHA256** using your shared secret 5. **Compare signatures** using a constant-time comparison function to prevent timing attacks 6. **Check the X-Webhook-ID** (optional but recommended) to prevent duplicate processing ### Implementation Examples [#implementation-examples] #### Python [#python] ```python import hmac import hashlib import time from flask import request, abort WEBHOOK_SECRET = "your_32_byte_secret_key" # 256-bit secret TOLERANCE_SECONDS = 300 # 5 minutes def verify_webhook(): # Get the raw request body raw_body = request.get_data() # Parse the signature header signature_header = request.headers.get('X-Webhook-Signature', '') parts = dict(item.split('=') for item in signature_header.split(',')) timestamp_str = parts.get('t') received_signature = parts.get('v1') if not timestamp_str or not received_signature: abort(401, "Missing signature components") # Verify timestamp to prevent replay attacks timestamp = int(timestamp_str) current_time = int(time.time()) if abs(current_time - timestamp) > TOLERANCE_SECONDS: abort(401, "Timestamp outside tolerance window") # Reconstruct the signed payload signed_payload = f"{timestamp_str}.{raw_body.decode('utf-8')}" # Compute the expected signature expected_signature = hmac.new( WEBHOOK_SECRET.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Constant-time comparison to prevent timing attacks if not hmac.compare_digest(expected_signature, received_signature): abort(401, "Invalid signature") # Optional: Check for duplicate webhook IDs webhook_id = request.headers.get('X-Webhook-ID') # Store and check webhook_id in your database to prevent duplicates return True ``` #### Node.js (Express) [#nodejs-express] ```javascript const crypto = require('crypto'); const express = require('express'); const WEBHOOK_SECRET = 'your_32_byte_secret_key'; // 256-bit secret const TOLERANCE_SECONDS = 300; // 5 minutes function verifyWebhook(req, res, next) { // Get raw body (ensure you're using express.raw() middleware) const rawBody = req.body; // Parse the signature header const signatureHeader = req.headers['x-webhook-signature'] || ''; const parts = {}; signatureHeader.split(',').forEach(part => { const [key, value] = part.split('='); parts[key] = value; }); const timestampStr = parts.t; const receivedSignature = parts.v1; if (!timestampStr || !receivedSignature) { return res.status(401).send('Missing signature components'); } // Verify timestamp to prevent replay attacks const timestamp = parseInt(timestampStr, 10); const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - timestamp) > TOLERANCE_SECONDS) { return res.status(401).send('Timestamp outside tolerance window'); } // Reconstruct the signed payload const signedPayload = `${timestampStr}.${rawBody.toString('utf-8')}`; // Compute the expected signature const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(signedPayload) .digest('hex'); // Constant-time comparison to prevent timing attacks if (!crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(receivedSignature) )) { return res.status(401).send('Invalid signature'); } // Optional: Check for duplicate webhook IDs const webhookId = req.headers['x-webhook-id']; // Store and check webhookId in your database to prevent duplicates next(); } // Use with express.raw() to preserve the raw body app.use('/webhooks', express.raw({ type: 'application/json' })); app.post('/webhooks', verifyWebhook, (req, res) => { // Process the webhook const payload = JSON.parse(req.body.toString()); // ... handle the webhook event res.sendStatus(200); }); ``` ## Security Best Practices [#security-best-practices] ### 1. Use HTTPS Only [#1-use-https-only] Always use HTTPS endpoints for webhooks. We reject HTTP URLs to prevent man-in-the-middle attacks. ### 2. Validate the Timestamp [#2-validate-the-timestamp] Always verify the timestamp is within an acceptable window (recommended: Β±5 minutes). This prevents replay attacks where an attacker attempts to resend a captured webhook. ### 3. Use Constant-Time Comparison [#3-use-constant-time-comparison] Always use constant-time comparison functions (like `hmac.compare_digest` in Python or `crypto.timingSafeEqual` in Node.js) when comparing signatures. This prevents timing attacks. ### 4. Store the Secret Securely [#4-store-the-secret-securely] * Generate a cryptographically secure random secret (32 bytes / 256 bits) * Store it encrypted using a key management service (KMS, HashiCorp Vault, etc.) * Never commit secrets to version control * Rotate secrets periodically ### 5. Implement Idempotency [#5-implement-idempotency] Use the `X-Webhook-ID` header to track which webhooks you've already processed. Store this ID in your database and reject duplicate deliveries. ### 6. Verify the Raw Body [#6-verify-the-raw-body] The signature is computed over the **raw request bytes**, not the parsed JSON. Make sure your verification code uses the exact body bytes received, before any parsing or transformation. ### 7. Return Appropriate Status Codes [#7-return-appropriate-status-codes] * Return `200` or `204` for successful processing * Return `401` for signature verification failures * Return `400` for malformed requests * Return `500` for internal server errors We will retry failed webhook deliveries with exponential backoff for status codes in the 5xx range. ## Troubleshooting [#troubleshooting] ### Signature Verification Fails [#signature-verification-fails] **Common causes:** * Using parsed JSON instead of raw request body for verification * Incorrect timestamp extraction or format * Secret key mismatch * Using string concatenation instead of proper byte operations * Not using UTF-8 encoding consistently **Solution:** * Ensure you're computing the HMAC over the exact raw bytes received * Verify your secret key matches what was configured * Check that you're extracting the timestamp correctly from the header * Use logging to compare your computed signature with the received signature ### Timestamp Out of Range [#timestamp-out-of-range] **Common causes:** * Server clock drift * Processing delays * Timezone issues **Solution:** * Synchronize your server clock using NTP * Use Unix timestamps (seconds since epoch) in UTC * Adjust your tolerance window if needed (but don't exceed 10 minutes) ### Duplicate Webhooks [#duplicate-webhooks] **Common causes:** * Network issues causing retries * Not implementing idempotency checks **Solution:** * Track `X-Webhook-ID` values in your database * Implement idempotent webhook handlers that can safely process the same event multiple times ## Testing Webhooks [#testing-webhooks] When testing webhook integration, you can use tools like: * **ngrok** or **localtunnel** to expose your local server to receive webhooks * **Request logging** to inspect the exact headers and body received * **Manual signature generation** to test your verification logic Contact support at **[contact@revrag.ai](mailto:contact@revrag.ai)** for assistance with testing webhooks in the staging environment. ## Support [#support] If you encounter any issues with webhook setup or signature verification, please contact our support team: **Email**: [contact@revrag.ai](mailto:contact@revrag.ai) --- # In-App agent > Learn how to integrate RevRag's In-App agent into your applications URL: /embed/embedded-agent-integration Markdown: /embed/embedded-agent-integration.md ## Quick Start [#quick-start] 1. **Choose your platform** from our supported integrations 2. **Install the SDK** for your selected platform 3. **Configure your API key** and initialize the agent 4. **Customize the interface** to match your brand ## Platform Support [#platform-support] * βœ… **React (Web)**: Full support with comprehensive documentation * βœ… **React Native**: Full support with comprehensive documentation * βœ… **Flutter**: Full support with comprehensive documentation * βœ…**Android Native**: Full support with comprehensive documentation * 🚧 **NodeJS**: Coming soon * 🚧 **iOS Native**: Coming soon ## Ready to Get Started? [#ready-to-get-started] {/*
πŸ“š
*/} Explore detailed documentation and features of the In-App agent
{/*
πŸš€
*/} Jump into Platform-specific integration guides and start building
*** **Need help?** Visit our [website](https://revrag.ai) or check out the detailed guides in the In-App agent tab. --- # Android Agent Integration > Drop-in brief for coding agents (Claude Code, Cursor, Copilot) to integrate the Revrag Embed Android SDK - phases, hard rules, API reference, and definition of done URL: /embed/integration/android-agent-integration Markdown: /embed/integration/android-agent-integration.md # Revrag Embed β€” Android integration, for a coding agent [#revrag-embed--android-integration-for-a-coding-agent] **How to use this file:** drop it anywhere in your Android repo and tell your coding agent (Claude Code, Cursor, Copilot, Windsurf, …): > Read `AGENT_INTEGRATION.md` and integrate the Revrag Embed SDK into this app. > My API key is ``. Everything the agent needs is below. A typical integration is 3 files and a few minutes. *** ## AGENT: START HERE [#agent-start-here] You are integrating the Revrag Embed Android SDK β€” a floating AI voice-agent button that follows the user across screens. Work through the phases in order. Do not skip Phase 0. ### Phase 0 β€” Gather what you cannot guess [#phase-0--gather-what-you-cannot-guess] Ask the user for anything in this table you cannot determine from the repo. Ask for **all** of it in one message, then proceed. | Needed | Why | If unknown | | -------------------------------- | ------------------------------ | --------------------------------------------- | | **API key** | required to initialize | **must ask β€” never invent one** | | **Environment URL** | key and environment are a pair | default `null` (production) | | **Which screens show the agent** | drives `allowedScreens` | **must ask** β€” do not guess from screen names | | **User id expression** | attaches events to a user | ask; use `""` if the app has no auth yet | Then determine from the repo yourself, without asking: 1. **Does an `Application` subclass exist?** Search for `: Application()` and `android:name` in `AndroidManifest.xml`. If none, you will create one. 2. **Which shape is this app?** Count the Activities FIRST, then look inside. Answer both questions before choosing β€” the shapes differ on both axes, and picking on UI toolkit alone lands on the wrong one: | Activities | Screens inside them | Shape | | ----------- | ------------------------------------------------------------- | ----- | | exactly one | Compose `setContent {}` + `NavHost` | **D** | | exactly one | `NavHostFragment` / fragment transactions | **B** | | several | one screen each, no nav host anywhere | **A** | | several | at least one has a `NavHostFragment` / `BottomNavigationView` | **C** | | several | at least one is Compose with `setContent {}` | **E** | A single-Activity app is D or B and never anything else. A multi-Activity app is A, C or E β€” and E only when a Compose Activity is involved; a multi-Activity XML app with tabs is C, not E. 3. **What are the screen names?** Read them, do not invent them: * Shape A β†’ Activity **class names** (`LoginActivity`) * Shape B/C β†’ nav graph `android:label` values, or fragment class names * Shape D/E β†’ NavHost **route** strings Report the list you found back to the user with your plan. ### Phase 1 β€” Dependency and permissions [#phase-1--dependency-and-permissions] **1a.** Add to the app module's `build.gradle.kts` (or `.gradle`): ```kotlin implementation("ai.revrag:embed-android:1.1.0") ``` `mavenCentral()` is already present in essentially every Android project β€” add it only if genuinely missing. Do **not** add any Compose dependency or the Compose compiler plugin; the SDK carries its own UI and works in pure-XML apps. **1b.** Add to `app/src/main/AndroidManifest.xml`, inside ``: ```xml ``` ⚠ **The SDK ships no permissions of its own.** If you skip this, the widget appears and every call fails. Do not write a runtime-permission flow β€” the SDK requests `RECORD_AUDIO` itself when the user first starts a call. **1c.** Verify `minSdk >= 24`, **Kotlin 2.0 or newer**, and that the project builds on JDK 17. The SDK is compiled with Kotlin 2.0.21 and its classes carry metadata 2.0 β€” a 1.9.x compiler fails with "the binary version of its metadata is 2.0.0, expected version is 1.9.0", and no flag works around it. If the project is on 1.9.x, say so and stop rather than attempting the integration. Ktor is shaded into `ai.revrag.shaded.ktor.*` and will not clash with the app's own Ktor. LiveKit, Lottie, Coil and Coroutines arrive transitively β€” do not add them yourself. ### Phase 2 β€” Initialize [#phase-2--initialize] Create the `Application` subclass if absent, register it in the manifest (``), and add: ```kotlin override fun onCreate() { super.onCreate() EmbedSDK.initialize(this, "") { result -> if (!result.success) Log.e("Embed", "init failed: ${result.error}") } EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "")) } ``` Import from `ai.revrag.embed.android.*` only. Never import `ai.revrag.embed.core.*` β€” it is internal and will break on upgrade. If the user gave an environment URL, pass it: `EmbedSDK.initialize(this, "", embedUrl = "") { … }` ### Phase 3 β€” Visibility config [#phase-3--visibility-config] Put this in the `Application`'s `companion object`. Use the **real** screen names you found in Phase 0.3 and the screens the user named in Phase 0. ```kotlin val EMBED_VISIBILITY = EmbedButtonVisibilityConfig( allowedScreens = listOf(/* names the user chose */), excludedScreens = listOf(/* splash, trampolines, deep-link handlers */), groups = listOf( EmbedButtonGroupConfig( id = "main", screens = listOf(/* screens that form one journey */), continuity = EmbedButtonContinuity.CONTINUOUS, delayMs = 500L, delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY ) ) ) ``` Rules you must follow: * **An empty `allowedScreens` means the widget shows EVERYWHERE.** Never leave it empty unless the user explicitly wants that. * Put splash/trampoline Activities in `excludedScreens`. Do **not** achieve this by skipping the attach there β€” that leaves a live call running. * Screens the user moves between within one task belong in one `CONTINUOUS` group, so the call survives navigation. * Names are matched **exactly** and are case-sensitive. ### Phase 4 β€” Mount [#phase-4--mount] Apply exactly one of the following, matching the shape from Phase 0.2. *** #### Shape A β€” XML, one Activity per screen [#shape-a--xml-one-activity-per-screen] In `Application.onCreate`, after `initialize`. **Touch no Activity file.** ```kotlin registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { EmbedProvider.attachOverlay( activity = activity as? ComponentActivity ?: return, appUserId = "", visibilityConfig = EMBED_VISIBILITY ) } override fun onActivityCreated(a: Activity, b: Bundle?) = Unit override fun onActivityStarted(a: Activity) = Unit override fun onActivityPaused(a: Activity) = Unit override fun onActivityStopped(a: Activity) = Unit override fun onActivitySaveInstanceState(a: Activity, b: Bundle) = Unit override fun onActivityDestroyed(a: Activity) = Unit }) ``` *** #### Shape B β€” XML, one Activity, many fragments [#shape-b--xml-one-activity-many-fragments] In that Activity's `onCreate`, **after `setContentView`**: ```kotlin val navHost = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navController = navHost.navController navController.addOnDestinationChangedListener { _, _, _ -> EmbedProvider.attachOverlay( activity = this, appUserId = "", visibilityConfig = MyApp.EMBED_VISIBILITY, navController = navController ) } ``` Use `findFragmentById(...) as NavHostFragment`, **not** `findNavController()` β€” the latter crashes when called from `onCreate` with a `FragmentContainerView`. If the app has no NavController, pass `currentScreen = ""` from wherever it swaps fragments instead, and omit `navController`. *** #### Shape C β€” XML, several Activities with several screens [#shape-c--xml-several-activities-with-several-screens] Both of the above. Create one helper so there is a single call site: ```kotlin // In the Application's companion object fun attachEmbed(activity: Activity) { val host = activity as? ComponentActivity ?: return EmbedProvider.attachOverlay( activity = host, appUserId = "", visibilityConfig = EMBED_VISIBILITY, navController = (activity as? MainActivity)?.embedNavController, currentScreen = null ) } ``` * `Application.onCreate` β†’ `registerActivityLifecycleCallbacks` with `onActivityResumed { attachEmbed(activity) }` (other overrides empty). * The tabbed Activity β†’ expose the controller and re-attach on destination change: ```kotlin val embedNavController: NavController? get() = if (::navController.isInitialized) navController else null navController.addOnDestinationChangedListener { _, _, _ -> MyApp.attachEmbed(this) } ``` `allowedScreens` will contain **both** Activity class names and nav labels. *** #### Shape D β€” Compose, single Activity [#shape-d--compose-single-activity] Wrap the existing content. Do not restructure the app. ```kotlin setContent { val navController = rememberNavController() val backStack by navController.currentBackStackEntryAsState() EmbedProviderComposable( currentScreen = backStack?.destination?.route ?: "", appUserId = "", visibilityConfig = EMBED_VISIBILITY, navController = navController ) { // the app's existing NavHost / content, unchanged } } ``` Screen names are **routes**. `allowedScreens` must use route strings. *** #### Shape E β€” Compose, several Activities [#shape-e--compose-several-activities] Use **Shape A**'s `Application` mount β€” it works for Compose Activities too and one mount serves them all. Add Shape C's `navController` hand-over for any Activity that has its own. Do **not** also use `EmbedProviderComposable`. *** ### Phase 5 β€” Dialogs (only if the app has them) [#phase-5--dialogs-only-if-the-app-has-them] For any `DialogFragment` / `BottomSheetDialogFragment` that should not cover the widget, add to `onStart()`: ```kotlin EmbedProvider.attachOverlay(dialog!!, requireActivity()) ``` Write no teardown β€” the widget returns to the Activity on every dismissal path. ### Phase 5b β€” Logout and position (do these if they apply) [#phase-5b--logout-and-position-do-these-if-they-apply] **If the app has authentication**, find the sign-out path and add: ```kotlin EmbedSDK.clearStorageCache() ``` Without it, one user's identity and conversation context carry into the next user's session on a shared device. This is a correctness requirement, not a nicety. **If any allowed screen has a bottom navigation bar, FAB or sticky CTA**, set an inset so the widget does not sit under it: ```kotlin EmbedButtonVisibilityConfig( defaultInset = EmbedButtonInset(right = 24, bottom = 80), // dp // …or per group: EmbedButtonGroupConfig(inset = EmbedButtonInset(bottom = 160)) ) ``` **If the app uses one NavController per bottom tab**, pass the ACTIVE controller, and re-attach when the tab changes as well as on destination change. ### Phase 5c β€” Event observability (do this β€” it is how you verify) [#phase-5c--event-observability-do-this--it-is-how-you-verify] Add this helper file to the app. It logs every SDK event with a readable label, and is the hook the user forwards to their own analytics later. ```kotlin import ai.revrag.embed.android.EmbedAnalyticsEvents import ai.revrag.embed.android.EmbedSDK import ai.revrag.embed.android.EventCallback import ai.revrag.embed.android.EventKeys import android.util.Log object SdkLifecycleListener { private const val TAG = "SdkLifecycle" private var callback: EventCallback? = null fun register(onEvent: ((name: String, data: Map) -> Unit)? = null) { if (callback != null) return val cb: EventCallback = { data -> val name = data["event_name"] as? String ?: "unknown" Log.d(TAG, "${label(name)} β†’ $data") onEvent?.invoke(name, data) } callback = cb EmbedSDK.on(EventKeys.ANALYTICS_DATA, cb) } fun unregister() { callback?.let { EmbedSDK.off(EventKeys.ANALYTICS_DATA, it) } callback = null } private fun label(name: String): String = when (name) { EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN -> "Widget expanded" EmbedAnalyticsEvents.AGENT_TAP_TO_CLOSE -> "Widget collapsed" EmbedAnalyticsEvents.AGENT_VISIBLE -> "Widget visible" EmbedAnalyticsEvents.AGENT_CONVERSATION_STARTED -> "Call started" EmbedAnalyticsEvents.AGENT_CONVERSATION_ENDED -> "Call ended" EmbedAnalyticsEvents.AVATAR_MODE_OPENED -> "Avatar opened" EmbedAnalyticsEvents.POPUP_MESSAGE_VISIBLE -> "Popup shown" EmbedAnalyticsEvents.MICROPHONE_PERMISSION_ALLOW -> "Mic permission granted" EmbedAnalyticsEvents.GEN_TOOL_TRIGGERED -> "Tool triggered" EmbedAnalyticsEvents.RAGE_CLICK -> "Rage click" EmbedAnalyticsEvents.FORM_EVENT -> "Form event" EmbedAnalyticsEvents.ERROR -> "SDK error" else -> name } } ``` Call `SdkLifecycleListener.register()` in `Application.onCreate` after `initialize`, and `unregister()` on logout. Use its `SdkLifecycle` log tag in Phase 6 to prove events flow. If the app registers `onAgent` / `onAvatarState` listeners anywhere, they must be deregistered in the matching `onDestroy` **with the same callback instance** β€” an inline lambda at the `off` call site removes nothing and leaks the Activity. ### Phase 6 β€” Verify, then report [#phase-6--verify-then-report] 1. **Build.** `./gradlew :app:assembleDebug`. Fix compile errors. 2. **Run and read logcat**, filtered to `RevragEmbed`. You are looking for: ``` [Screen] SplashActivity#1 β†’ LoginActivity#2 [Visibility] 'SplashActivity' NOT allowed β€” hidden [Visibility] 'LoginActivity' delay elapsed β€” showing ``` 3. **Check `[Init]`:** * `widgetConfig: null` or `backend returned NO widget_config` β†’ the API key has no widget provisioned for that environment. This is **not** a code problem. Tell the user to confirm the key/environment pair with Revrag, and stop β€” do not try to work around it. 4. **Report to the user:** which shape you detected, the screen names you used, the files you changed, and what you saw in logcat. *** ## HARD RULES [#hard-rules] Violating these produces bugs that look like SDK faults and are hard to trace. 1. **Never write teardown.** No `detach()` in `onPause`/`onDestroy`/`onStop`. The next Activity claims the widget before the previous one dies; the SDK owns the hand-off. Teardown code is the single most common cause of "the widget disappears mid-flow". 2. **One mount mechanism per app.** `attachOverlay` **or** `EmbedProviderComposable`. Both = two widgets. 3. **Always use named arguments** on `attachOverlay` and `EmbedProviderComposable`. The parameter lists grow; positional calls break on upgrade. 4. **Import only `ai.revrag.embed.android.*`.** Never `ai.revrag.embed.core.*`. 5. **`attachOverlay` must run after `setContentView()`.** From `onResume` this is automatic; from `onCreate` it is your responsibility. 6. **Never invent an API key**, and never fall back to a placeholder to "make it build". Ask. 7. **Do not add `RECORD_AUDIO` handling yourself** β€” the SDK requests it. Just declare it in the manifest. 8. **Do not use an empty `allowedScreens`** to mean "the screens I listed". Empty means everywhere. 9. **Do not rename the user's screens** to match a config you wrote. Read the real names and write the config to match them. 10. **Do not turn off `autoTrackActivities`.** It defaults to `true`, and off means a forgotten Activity keeps a live microphone on a screen the product excluded. *** ## API REFERENCE [#api-reference] Everything below is `ai.revrag.embed.android.*`. ```kotlin // Initialize β€” once, in Application.onCreate EmbedSDK.initialize( context: Context, apiKey: String, embedUrl: String? = null, // null = production autoTrackActivities: Boolean = true, // leave true onResult: ((InitResult) -> Unit)? = null ) // Identify the user EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "user-123")) // Mount / move / rename β€” XML and Compose Activities alike EmbedProvider.attachOverlay( activity: ComponentActivity, // required appUserId: String = "", appVersion: String = "", accentColor: Int = 0xFF6C63FF.toInt(), visibilityConfig: EmbedButtonVisibilityConfig = EmbedButtonVisibilityConfig(), navController: NavController? = null, currentScreen: String? = null, chatPanelUrl: String? = null ): EmbedOverlayHandle? // Lift the widget into a dialog's window EmbedProvider.attachOverlay(dialog: Dialog, activity: ComponentActivity) EmbedProvider.attachOverlay(window: Window, activity: ComponentActivity) // Compose hosts β€” single Activity @Composable fun EmbedProviderComposable( currentScreen: String, appUserId: String = "", accentColor: Color = Color(0xFF6C63FF), visibilityConfig: EmbedButtonVisibilityConfig = EmbedButtonVisibilityConfig(), navController: NavController? = null, chatPanelUrl: String? = null, content: (@Composable () -> Unit)? = null ) // Rename the current screen at any time handle.setCurrentScreen("Checkout") // ── Programmatic control (optional β€” the widget drives itself) ────────────── EmbedSDK.startCall(activity) // requests RECORD_AUDIO if needed EmbedSDK.startCall(activity, agentTriggerMode: AgentTriggerMode? = null) { started: Boolean -> } EmbedSDK.endCall() EmbedSDK.isCallActive(): Boolean // agentTriggerMode β€” WHO OWNS THE UI. A host decision, not the backend's. // AgentTriggerMode.CO_PILOT β€” your app's screens are the UI; the SDK draws // only its button. The usual choice. // AgentTriggerMode.WORKFLOW β€” Revrag draws its own panel (avatar screen with // Voice / Chat / Avatar tabs). // ⚠ Passed at startCall ONLY, never at initialize. A call the USER starts by // tapping the button carries no mode β€” only calls the host starts can carry // it. It does NOT enable video; that is media_mode in the backend's config. // ⚠ DEFAULT IS CO_PILOT. Omitting the argument, passing null, or a // user-initiated call all resolve to CO_PILOT + AUDIO. Do not pass // CO_PILOT explicitly to "make sure" β€” omit it. Pass WORKFLOW only when the // user has said Revrag should draw its own screen. EmbedSDK.collapseWidget() // dismiss the card, keep the call EmbedSDK.isWidgetExpanded // StateFlow EmbedSDK.minimizeAvatar() // leave full-screen avatar mode EmbedSDK.isAvatarOpen // StateFlow // ── Lifecycle callbacks ──────────────────────────────────────────────────── EmbedSDK.onAgent(AgentEvent.AGENT_CONNECTED) { } EmbedSDK.onAgent(AgentEvent.AGENT_DISCONNECTED) { data -> } // carries duration EmbedSDK.onAvatarState { isOpen -> } EmbedSDK.offAgent(...) / EmbedSDK.offAvatarState(...) // deregister! // AgentEvent: AGENT_CONNECTED, AGENT_DISCONNECTED, POPUP_MESSAGE_VISIBLE // ── Readiness ────────────────────────────────────────────────────────────── EmbedSDK.isInitialized(): Boolean // has credentials EmbedSDK.isInitializedFlow // StateFlow EmbedSDK.widgetConfig // StateFlow β€” non-null = can draw // ── Context on every event ───────────────────────────────────────────────── EmbedSDK.setCurrentFlow("loan_application") EmbedSDK.setAppVersion("4.2.0") // only if yours differs from PackageInfo // EventKeys: USER_DATA, SCREEN_STATE, ANALYTICS_DATA, CUSTOM_EVENT // ── Listening to events ──────────────────────────────────────────────────── val cb: EventCallback = { data -> } EmbedSDK.on(EventKeys.ANALYTICS_DATA, cb) EmbedSDK.off(EventKeys.ANALYTICS_DATA, cb) // SAME instance, or it does not unhook // Everything the SDK fires arrives on ANALYTICS_DATA keyed by "event_name": // EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN / AGENT_TAP_TO_CLOSE / AGENT_VISIBLE // AGENT_CONVERSATION_STARTED / AGENT_CONVERSATION_ENDED / AVATAR_MODE_OPENED // POPUP_MESSAGE_VISIBLE / GEN_TOOL_TRIGGERED / MICROPHONE_PERMISSION_ALLOW // RAGE_CLICK / FORM_EVENT / ERROR // Compare against the constants, never string literals. // ── ⚠ On logout β€” REQUIRED if the app has auth ───────────────────────────── EmbedSDK.clearStorageCache() ``` **Button position** ```kotlin EmbedButtonInset(right = 24, bottom = 80, left = 24, top = 0) // dp from edges ``` Set `defaultInset` on the visibility config, or `inset` on a group to override it for that group's screens. Use it whenever a bottom navigation bar, FAB or sticky CTA would sit under the widget. **Config types** ```kotlin EmbedButtonVisibilityConfig( allowedScreens: List = emptyList(), // EMPTY = everywhere excludedScreens: List = emptyList(), // wins over allowedScreens showDelay: Long = 0L, groups: List = emptyList(), endCallWhenHiddenByVisibility: Boolean = true ) EmbedButtonGroupConfig( id: String, screens: List, continuity: EmbedButtonContinuity = EmbedButtonContinuity.PER_SCREEN, delayMs: Long = 0L, delayPolicy: EmbedButtonDelayPolicy = EmbedButtonDelayPolicy.PER_SCREEN ) enum EmbedButtonContinuity { PER_SCREEN, CONTINUOUS } enum EmbedButtonDelayPolicy { PER_SCREEN, ONCE_PER_GROUP_ENTRY, ONCE_PER_APP_SESSION } ``` **Screen-name resolution**, highest wins: 1. `currentScreen` / `handle.setCurrentScreen(...)` β€” explicit 2. `NavController` destination `android:label` or route 3. Fragment class name 4. Activity class name β€” the floor, never absent Naming one screen by hand disables rung 3 for the session: if you start naming, name them all. *** ## TROUBLESHOOTING (for the agent) [#troubleshooting-for-the-agent] | Symptom | Cause | Fix | | ---------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------- | | Button never appears; `[Init] widgetConfig: null` | key has no widget for that environment | not a code issue β€” tell the user to check key/environment with Revrag | | Button never appears; `[Visibility] 'X' NOT allowed` | `X` is not in `allowedScreens`, or is excluded | make the config match the reported name exactly, case included | | Button never appears; no `[Visibility]` line at all | mount never ran | `attachOverlay` missing, or called before `setContentView` | | Appears everywhere | `allowedScreens` empty | list the screens | | Disappears mid-flow | teardown code | remove `detach()` from lifecycle callbacks | | Two buttons | two mount mechanisms | keep one | | Call never starts | `RECORD_AUDIO` not declared | add it to the manifest | | Crash: `findNavController` in `onCreate` | `FragmentContainerView` timing | use `findFragmentById(...) as NavHostFragment` | | Unresolved reference on upgrade | positional args, or a `core` import | use named args; import only `…embed.android.*` | *** ## DEFINITION OF DONE [#definition-of-done] * [ ] Dependency added; `minSdk >= 24` * [ ] `INTERNET` + `RECORD_AUDIO` in the manifest * [ ] `EmbedSDK.initialize` in `Application.onCreate`; `Application` registered * [ ] Exactly one mount mechanism, matching the detected shape * [ ] `allowedScreens` non-empty and matching real reported names * [ ] Splash/trampolines in `excludedScreens` * [ ] No teardown code anywhere for the widget β€” but any `onAgent` / `onAvatarState` listener you registered is deregistered with its owner * [ ] `EmbedSDK.clearStorageCache()` wired into logout, if the app has auth * [ ] Inset set if a bottom bar / sticky CTA overlaps an allowed screen * [ ] `./gradlew :app:assembleDebug` succeeds * [ ] Logcat shows `delay elapsed β€” showing` on an allowed screen and `NOT allowed β€” hidden` on an excluded one * [ ] `SdkLifecycle` log tag shows events firing (`Widget visible` at minimum) * [ ] `USER_DATA` sent with a real `app_user_id` before any call * [ ] Reported to the user: shape, screen names, files changed, logcat evidence Full human-readable guide: [Android Integration Guide](/embed/integration/android-native) --- # Android App Size Optimization > Reduce APK size by enabling ABI splits when using LiveKit. URL: /embed/integration/android-app-size-optimization Markdown: /embed/integration/android-app-size-optimization.md # Android App Size Optimization [#android-app-size-optimization] Because this SDK relies on LiveKit (WebRTC) for real-time audio/video, it includes native C++ binaries. You may notice your Universal (Debug) APK size increase by approximately 70MB. This is normal. By default, Android bundles binaries for every architecture (older phones, modern phones, and PC emulators) into a single file. Your production users should not download this "dead weight." To ensure your users only download the \~20MB required for their specific device, add the following configuration to your `android/app/build.gradle` file: ```groovy android { // ... existing config splits { abi { enable true reset() // Includes only real device architectures (removes emulators like x86) include "armeabi-v7a", "arm64-v8a" // Prevents generating a giant "Universal" APK containing all binaries universalApk false } } } ``` ## Why is this necessary? [#why-is-this-necessary] This configuration enables ABI (Application Binary Interface) splitting. Here is exactly what it does: * `reset()`: Clears Android's default build list, allowing us to define a custom list of supported devices. * `include "armeabi-v7a", "arm64-v8a"`: Tells the build system to only generate binaries for physical Android devices (32-bit and 64-bit). It strips out x86 and x86\_64 binaries (used only for emulators), which saves \~30-40MB immediately. * `universalApk false`: Ensures the build system does not create a "fat APK" that contains every architecture. ## The Result [#the-result] Instead of one giant \~75MB file, the system generates separate, smaller slices (\~25MB) for each device type. ## Note for Play Store Deployment [#note-for-play-store-deployment] If you build using Android App Bundles (`.aab`), Google Play handles this splitting automatically. The configuration above is critical if you are distributing APKs manually or want to control the split logic explicitly. --- # Android Native > Android SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (Kotlin, XML & Jetpack Compose) URL: /embed/integration/android-native Markdown: /embed/integration/android-native.md # Android Integration Guide [#android-integration-guide] The complete guide for adding the Revrag AI voice agent to an Android app. One floating button that follows your user across every screen, keeps its position, and keeps a live call running while they navigate. **Current version: `1.1.0`** Β· minSdk 24 Β· JDK 17 Β· Kotlin Whatever your app looks like β€” XML or Compose, one Activity or fifty β€” the integration is the same three calls. Section 5 shows the exact shape for your app; everything before it applies to everyone. Get your API key from [https://app.revrag.ai](https://app.revrag.ai). *** ## Table of contents [#table-of-contents] 1. [Install](#1-install) β€” dependency and permissions 2. [Initialize](#2-initialize) β€” one call, in `Application` 3. [Mount](#3-mount) β€” one call, and the SDK follows the user 4. [Decide where it appears](#4-decide-where-it-appears) β€” the visibility config 5. [Your app's shape](#5-your-apps-shape) β€” **pick one**, complete code 6. [Dialogs and bottom sheets](#6-dialogs-and-bottom-sheets) Β· [Position the button](#6b-position-the-button) Β· [Control it from your code](#6c-control-it-from-your-own-code) Β· [Events and analytics](#6d-events-and-analytics) Β· [A drop-in event logger](#6e-a-drop-in-event-logger) 7. [How screens get their names](#7-how-screens-get-their-names) 8. [Verify it works](#8-verify-it-works) 9. [Troubleshooting](#9-troubleshooting) 10. [Checklist](#10-checklist) 11. [Upgrading](#11-upgrading) *** ## 1. Install [#1-install] ### Dependency [#dependency] ```kotlin // app/build.gradle.kts β€” mavenCentral() is already in every Android project dependencies { implementation("ai.revrag:embed-android:1.1.0") } ``` No Compose toolchain is required. If your app is pure XML you do not need the Compose compiler plugin, `buildFeatures.compose`, or any Compose dependency β€” the SDK brings its own UI. ### Permissions β€” do not skip this [#permissions--do-not-skip-this] **The SDK ships no permissions of its own.** Nothing is merged into your manifest, by design: an SDK should not silently add a microphone permission to your app. You declare them: ```xml ``` Without `RECORD_AUDIO` the widget appears and the call fails to start. The SDK requests the runtime permission itself when the user first starts a call β€” you do not need to write a permission flow. ### Requirements [#requirements] | | | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `minSdk` | 24 (Android 7.0) | | JDK | 17 | | Kotlin | **2.0 or newer** β€” the SDK is compiled with 2.0.21 and its classes carry Kotlin metadata 2.0, which a 1.9.x compiler refuses to read ([section 9](#9-troubleshooting)) | | Activity type | `ComponentActivity` or any subclass β€” `AppCompatActivity` qualifies | | API key | from Revrag, per environment | **What it pulls in.** LiveKit (WebRTC), Lottie, Coil and Kotlin Coroutines arrive transitively. **Ktor is shaded** into `ai.revrag.shaded.ktor.*` and does not appear in your dependency tree, so it cannot clash with your own Ktor version. **Your API key and environment are a pair.** A widget is provisioned per key per environment. A key from the dev backend returns no widget on production and the button will never appear β€” with no error, because "this key has no widget here" is a valid answer. If the button never shows, [section 9](#9-troubleshooting) starts here. *** ## 2. Initialize [#2-initialize] Once, in `Application.onCreate` β€” not in an Activity. Initializing in an Activity means screens shown before it can never display the agent. ```kotlin class MyApp : Application() { override fun onCreate() { super.onCreate() // Third argument is embedUrl β€” omit it for production, or pass your // environment's URL. It must match the environment your key belongs to. EmbedSDK.initialize(this, "YOUR_API_KEY") { result -> if (!result.success) Log.e("Embed", "init failed: ${result.error}") } } } ``` Register it in the manifest: ```xml ``` ### Identify your user [#identify-your-user] ```kotlin EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "user-123")) ``` *** ## 3. Mount [#3-mount] `EmbedProvider.attachOverlay` is the only integration call, and it is safe from every Activity's `onResume`: * the **first** call mounts the widget; * a call from a **new** Activity moves the same widget into that window β€” position, expanded card and live call intact; * a repeat call in the same window just updates the screen name. The SDK registers its own Activity observer, so it moves the widget and names screens on its own. The one thing it deliberately will **not** do is the *first* mount β€” mounting carries your `appUserId` and `visibilityConfig`, and doing it for you would use neither. ```kotlin // In MyApp.onCreate(), alongside initialize() registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { EmbedProvider.attachOverlay( activity = activity as? ComponentActivity ?: return, appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY // section 4 ) } // all other overrides empty }) ``` Attaching from every resume is simply the cheapest way to say "whichever Activity is first" β€” the second call onward costs nothing. **Do not** detach in `onPause` or `onDestroy`. The incoming Activity claims the widget before the outgoing one dies, and the SDK owns the hand-off. Teardown code is the most common cause of a widget that vanishes mid-flow. `attachOverlay` must run **after** `setContentView()`. `onResume` always satisfies this. ### Full parameter list [#full-parameter-list] ```kotlin EmbedProvider.attachOverlay( activity = this, // ComponentActivity β€” required appUserId = "user-123", // your stable user id visibilityConfig = EMBED_VISIBILITY, // where the widget may appear navController = null, // pass it if this Activity has one currentScreen = null, // an explicit name, if you want one accentColor = 0xFF6C63FF.toInt(), // @ColorInt chatPanelUrl = null // your own chat page, optional ) ``` All parameters except `activity` have defaults. **Use named arguments** β€” the list grows over time and positional calls are how integrations break on upgrade. *** ## 4. Decide where it appears [#4-decide-where-it-appears] You do not name your screens; you *decide about* them. Names come from your Activity or destination automatically ([section 7](#7-how-screens-get-their-names)). ```kotlin val EMBED_VISIBILITY = EmbedButtonVisibilityConfig( // Where the widget MAY appear. Anywhere else it is hidden β€” and a live call // ends, because leaving the allowlist is leaving the agent. allowedScreens = listOf("LoginActivity", "Home", "Plans"), // Where it must NOT appear. Use this rather than "skip the attach there": // it also ends an active call, which skipping does not. excludedScreens = listOf("SplashActivity"), groups = listOf( EmbedButtonGroupConfig( id = "main_tabs", screens = listOf("Home", "Plans"), // CONTINUOUS = one journey: the widget stays up across these screens // and a voice call SURVIVES every move inside the group. continuity = EmbedButtonContinuity.CONTINUOUS, delayMs = 500L, // entrance delay delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY ) ) ) ``` ### What this buys you, with no further code [#what-this-buys-you-with-no-further-code] | User does | Widget does | | --------------------------------------------- | -------------------------------------------------------------------------------- | | Moves between screens in one CONTINUOUS group | Stays visible β€” no blink, no re-delay; a live call keeps running | | Crosses into another group | One clean entrance with that group's `delayMs`; the previous call ends by design | | Opens a screen not in `allowedScreens` | Hides, and an active call ends | | Opens a screen in `excludedScreens` | Hides, and an active call ends | | Rotates, or the system recreates an Activity | Reappears immediately β€” delays are never replayed | | Backgrounds and returns | Same state, same position | ### All options [#all-options] **`EmbedButtonVisibilityConfig`** | Field | Default | Meaning | | ------------------------------- | ------------- | ---------------------------------------------------------------------------------------------- | | `allowedScreens` | `emptyList()` | Screens where the widget may appear. **Empty means everywhere.** | | `excludedScreens` | `emptyList()` | Screens where it never appears. Wins over `allowedScreens`. | | `showDelay` | `0L` | Default entrance delay in ms, when a group does not set one. | | `groups` | `emptyList()` | Journeys β€” see below. | | `defaultInset` | SDK default | Starting position of the button, in dp from each edge ([section 6B](#6b-position-the-button)). | | `endCallWhenHiddenByVisibility` | `true` | End a live call when the widget hides. Leave on unless you know why not. | **`EmbedButtonGroupConfig`** | Field | Default | Meaning | | ------------- | ------------ | ----------------------------------------------------------------------------------- | | `id` | β€” | Any stable string. | | `screens` | β€” | The screens in this journey. | | `continuity` | `PER_SCREEN` | `CONTINUOUS` keeps the widget and the call alive across the group. | | `inset` | `null` | Position override for this group's screens ([section 6B](#6b-position-the-button)). | | `delayMs` | `0L` | Entrance delay for this group. | | `delayPolicy` | `PER_SCREEN` | `PER_SCREEN`, `ONCE_PER_GROUP_ENTRY`, or `ONCE_PER_APP_SESSION`. | An `allowedScreens` entry that no screen ever reports does not crash β€” it silently hides the widget. [Section 8](#8-verify-it-works) shows how to see the name the SDK actually has. *** ## 5. Your app's shape [#5-your-apps-shape] Find your app below. Each is complete and copy-pasteable. | Your app | Go to | | ------------------------------------------------------ | ----------------------------------------------------------- | | XML, one Activity per screen | [5A](#5a--xml-one-activity-per-screen) | | XML, one Activity hosting fragments / tabs | [5B](#5b--xml-one-activity-many-screens) | | XML, several Activities that each host several screens | [5C](#5c--xml-several-activities-each-with-several-screens) | | Jetpack Compose, single Activity + NavController | [5D](#5d--jetpack-compose-single-activity) | | Jetpack Compose, several Activities | [5E](#5e--jetpack-compose-several-activities) | | Bottom tabs with one NavHost per tab | [5F](#5f--bottom-tabs-with-one-navhost-per-tab) | *** ### 5A β€” XML, one Activity per screen [#5a--xml-one-activity-per-screen] The classic case. **Your Activities need zero Embed code**; the whole integration is one file. ```kotlin // MyApp.kt β€” the entire integration package com.example.bank import ai.revrag.embed.android.* import android.app.Activity import android.app.Application import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity class MyApp : Application() { override fun onCreate() { super.onCreate() // 1) Initialize once, before any Activity. EmbedSDK.initialize(this, "YOUR_API_KEY") { result -> if (!result.success) Log.e("Embed", "init failed: ${result.error}") } EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "user-123")) // 2) Mount. The SDK follows the user from here on and names every // screen after its Activity class. registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { EmbedProvider.attachOverlay( activity = activity as? ComponentActivity ?: return, appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY ) } override fun onActivityCreated(a: Activity, b: Bundle?) = Unit override fun onActivityStarted(a: Activity) = Unit override fun onActivityPaused(a: Activity) = Unit override fun onActivityStopped(a: Activity) = Unit override fun onActivitySaveInstanceState(a: Activity, b: Bundle) = Unit override fun onActivityDestroyed(a: Activity) = Unit }) } companion object { // 3) Screen names ARE your Activity class names. val EMBED_VISIBILITY = EmbedButtonVisibilityConfig( allowedScreens = listOf( "LoginActivity", "HomeActivity", "PlansActivity" ), excludedScreens = listOf("SplashActivity"), groups = listOf( EmbedButtonGroupConfig( id = "main", screens = listOf("HomeActivity", "PlansActivity"), continuity = EmbedButtonContinuity.CONTINUOUS, delayMs = 500L, delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY ) ) ) } } ``` Your Activities stay exactly what they are: ```kotlin class HomeActivity : AppCompatActivity() { // zero Embed code override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) } } ``` *** ### 5B β€” XML, one Activity, many screens [#5b--xml-one-activity-many-screens] One Activity swapping fragments or nav-graph destinations. Hand the SDK your `NavController` **once** and each destination names itself from its `android:label`. ```kotlin class MainActivity : AppCompatActivity() { private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // FragmentContainerView-safe lookup β€” findNavController() from onCreate // is the classic crash. val navHost = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHost.navController // Re-attach on each destination change: same window, so this is just a // screen update. No name is passed β€” the controller is authoritative. navController.addOnDestinationChangedListener { _, _, _ -> EmbedProvider.attachOverlay( activity = this, appUserId = "user-123", visibilityConfig = MyApp.EMBED_VISIBILITY, navController = navController ) } } } ``` Your nav graph supplies the names: ```xml ``` ```kotlin allowedScreens = listOf("Home", "Plans") // Account omitted β†’ hidden there ``` **No NavController?** If you swap fragments manually, tell the SDK yourself: ```kotlin EmbedProvider.attachOverlay( activity = this, appUserId = "user-123", visibilityConfig = MyApp.EMBED_VISIBILITY, currentScreen = "Home" ) ``` Naming even one screen by hand takes ownership: automatic fragment naming stands down for the whole session. If you start naming, name them all. *** ### 5C β€” XML, several Activities each with several screens [#5c--xml-several-activities-each-with-several-screens] Combine 5A and 5B: mount from `Application` for the Activities that are whole screens, and hand over the `NavController` from the Activity that has one. ```kotlin // MyApp.kt object EmbedIntegration { fun attach(activity: Activity) { val host = activity as? ComponentActivity ?: return EmbedProvider.attachOverlay( activity = host, appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY, // Only the tabbed Activity has one. Everything else passes null, // which hands naming back to the SDK. navController = (activity as? MainActivity)?.embedNavController, currentScreen = null ) } val EMBED_VISIBILITY = EmbedButtonVisibilityConfig( allowedScreens = listOf( "LoginActivity", "VerifyPanActivity", // Activity class names "Home", "Plans" // nav-graph labels ), excludedScreens = listOf("SplashActivity"), groups = listOf( EmbedButtonGroupConfig( id = "onboarding", screens = listOf("LoginActivity", "VerifyPanActivity"), continuity = EmbedButtonContinuity.CONTINUOUS, delayMs = 500L, delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY ), EmbedButtonGroupConfig( id = "main_tabs", screens = listOf("Home", "Plans"), continuity = EmbedButtonContinuity.CONTINUOUS, delayMs = 500L, delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY ) ) ) } ``` ```kotlin // MyApp.onCreate β€” mount from every resume registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) = EmbedIntegration.attach(activity) /* other overrides empty */ }) ``` ```kotlin // MainActivity β€” expose the controller and re-attach on destination change class MainActivity : AppCompatActivity() { private lateinit var navController: NavController val embedNavController: NavController? get() = if (::navController.isInitialized) navController else null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) navController = (supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController navController.addOnDestinationChangedListener { _, _, _ -> EmbedIntegration.attach(this) } } } ``` Naming ownership follows whoever holds the widget: it moves to the controller on the way into the tabs, and back to the SDK on the way out. Working example: `examples/android-xml` in the [SDK repository](https://github.com/revrag-ai/embed-android). *** ### 5D β€” Jetpack Compose, single Activity [#5d--jetpack-compose-single-activity] Wrap your content in `EmbedProviderComposable` and pass your `NavController`. ```kotlin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val navController = rememberNavController() val backStack by navController.currentBackStackEntryAsState() EmbedProviderComposable( currentScreen = backStack?.destination?.route ?: "Home", appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY, navController = navController ) { NavHost(navController, startDestination = "home") { composable("home") { HomeScreen() } composable("plans") { PlansScreen() } } } } } } ``` Screen names are your **routes** (`"home"`, `"plans"`), so spell `allowedScreens` the same way: ```kotlin allowedScreens = listOf("home", "plans") ``` **No NavController?** Drive it from your own state β€” any string works: ```kotlin var screen by remember { mutableStateOf("home") } EmbedProviderComposable(currentScreen = screen, ... ) { /* content */ } ``` *** ### 5E β€” Jetpack Compose, several Activities [#5e--jetpack-compose-several-activities] Use the XML mount (`EmbedProvider.attachOverlay`) from `Application`, exactly as in [5A](#5a--xml-one-activity-per-screen) β€” it works for Compose Activities too, and one mount serves all of them. Then, inside any Activity that has its own `NavController`, hand it over as in 5C. ```kotlin // Application β€” mount once, SDK follows override fun onActivityResumed(activity: Activity) { EmbedProvider.attachOverlay( activity = activity as? ComponentActivity ?: return, appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY, navController = (activity as? MainActivity)?.embedNavController ) } ``` Do **not** also wrap that Activity's content in `EmbedProviderComposable` β€” that would mount a second widget. Pick one mechanism per app. *** ### 5F β€” Bottom tabs with one NavHost per tab [#5f--bottom-tabs-with-one-navhost-per-tab] If each tab owns its own `NavController`, pass **the active one**. Re-attach when the tab changes as well as when a destination inside it changes: ```kotlin // whenever the selected tab OR its destination changes EmbedProvider.attachOverlay( activity = this, appUserId = "user-123", visibilityConfig = EMBED_VISIBILITY, navController = controllerForSelectedTab ) ``` Nested graphs need nothing extra β€” a nested destination reports its own label/route. *** ## 6. Dialogs and bottom sheets [#6-dialogs-and-bottom-sheets] A dialog gets its own window, which always paints above the Activity's β€” so an untouched widget would be covered by it. Lift it in for as long as the dialog shows: ```kotlin // e.g. in DialogFragment.onStart() / BottomSheetDialogFragment.onStart() EmbedProvider.attachOverlay(dialog!!, requireActivity()) ``` The widget returns to the Activity automatically on every dismissal path β€” you do not write teardown. For a raw `Window`: ```kotlin EmbedProvider.attachOverlay(window, activity) ``` *** ## 6B. Position the button [#6b-position-the-button] The widget is draggable, but you choose where it starts. Insets are in **dp** from each edge: ```kotlin EmbedButtonVisibilityConfig( defaultInset = EmbedButtonInset(right = 24, bottom = 80), groups = listOf( EmbedButtonGroupConfig( id = "checkout", screens = listOf("Cart", "Payment"), // lift it above this flow's sticky "Pay" bar inset = EmbedButtonInset(right = 24, bottom = 160) ) ) ) ``` `EmbedButtonInset(right, bottom, left, top)` β€” a group's `inset` overrides `defaultInset` for its screens. Use it wherever a bottom bar, FAB or sticky CTA would otherwise sit under the widget. *** ## 6C. Control it from your own code [#6c-control-it-from-your-own-code] The widget drives itself, but everything is available programmatically. ### Start and end calls [#start-and-end-calls] ```kotlin EmbedSDK.startCall(activity) // requests RECORD_AUDIO if needed EmbedSDK.endCall() EmbedSDK.isCallActive() // Boolean // with a completion callback EmbedSDK.startCall(activity) { started -> if (!started) { /* could not start β€” the reason is already reported */ } } ``` #### Who owns the UI β€” `agentTriggerMode` [#who-owns-the-ui--agenttriggermode] A call can run in one of two modes, and **this is the host's decision, not the backend's** β€” whether Revrag may draw its own screen over your app is your integration's call, so it lives in your code: ```kotlin EmbedSDK.startCall(activity, AgentTriggerMode.CO_PILOT) EmbedSDK.startCall(activity, AgentTriggerMode.WORKFLOW) { started -> } ``` | Mode | Who draws the UI | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `CO_PILOT` | **Your app.** The agent works inside your own screens and dialogs; the SDK draws nothing but its button. The natural state is the expanded button with your app visible. | | `WORKFLOW` | **Revrag.** The agent drives a panel the SDK renders β€” the avatar screen with its Voice / Chat / Avatar tabs. | Three things worth knowing: * **It is passed at `startCall` only β€” never at `initialize`.** There is no app-level default to declare. * **A call the USER starts by tapping the button carries no mode**, because there is no host frame to supply one, so it takes the default below. Only calls *your code* starts can carry this value. If you need every call in one mode, start calls yourself rather than relying on the button. * **It does not turn video on.** Video is the backend's half β€” it needs `media_mode: "video"` in the widget config. Two declarations, from two places, because they answer different questions: *who owns the screen* and *what media the call uses*. **The default is `CO_PILOT`.** Omitting the argument, passing `null`, or letting the user start the call from the button all resolve to: | | | | ------------------ | ---------------------------------------------------------- | | `agentTriggerMode` | `CO_PILOT` β€” your app owns the UI | | `mediaMode` | `AUDIO`, unless the backend's widget config says otherwise | | avatar surface | full page, when the user opens the avatar | So the SDK never draws its own screen unless you ask for `WORKFLOW` or the user opens the avatar themselves. If co-pilot is what you want β€” and for most apps it is β€” you do not need to pass anything. Every call logs which mode it actually ran in: ``` call mode LATCHED action=CO_PILOT media=AUDIO surface=FULL_PAGE video=false ``` One quirk worth knowing: co-pilot has no chat surface, so a co-pilot call whose backend config says `media_mode: "chat"` runs as audio and logs `media_mode=chat has no co-pilot surface β€” running as audio`. It downgrades rather than failing the call. ### The widget card [#the-widget-card] ```kotlin EmbedSDK.collapseWidget() // dismiss the card, keep the call EmbedSDK.isWidgetExpanded // StateFlow EmbedSDK.minimizeAvatar() // leave full-screen avatar mode EmbedSDK.isAvatarOpen // StateFlow ``` ### Send text to the agent [#send-text-to-the-agent] ```kotlin EmbedSDK.sendText("Show me my EMI schedule") ``` ### Know when a call starts and ends [#know-when-a-call-starts-and-ends] ```kotlin private val onConnected: AgentEventCallback = { /* … */ } EmbedSDK.onAgent(AgentEvent.AGENT_CONNECTED, onConnected) EmbedSDK.onAgent(AgentEvent.AGENT_DISCONNECTED) { data -> // data carries call duration } EmbedSDK.onAvatarState { isOpen -> /* full-screen avatar opened/closed */ } ``` Deregister with `offAgent(...)` / `offAvatarState(...)` when your listener's owner dies β€” a listener held by a destroyed screen is a leak. `AgentEvent`: `AGENT_CONNECTED`, `AGENT_DISCONNECTED`, `POPUP_MESSAGE_VISIBLE`. ### Readiness [#readiness] ```kotlin EmbedSDK.isInitialized() // Boolean, right now EmbedSDK.isInitializedFlow // StateFlow, to observe EmbedSDK.widgetConfig // StateFlow β€” non-null = can render ``` `isInitializedFlow` means "the SDK has credentials", which cannot fail. To know the **button can actually be drawn**, observe `widgetConfig` β€” see [section 9](#9-troubleshooting). ### On logout [#on-logout] ```kotlin EmbedSDK.clearStorageCache() ``` **Call this whenever the user signs out.** Without it, stored identity and conversation context carry into the next user's session on a shared device. *** ## 6D. Events and analytics [#6d-events-and-analytics] Everything the widget does is observable, and you can push your own context in. ### Sending events β€” `EmbedSDK.event(key, data)` [#sending-events--embedsdkeventkey-data] Four keys, each with a shape the backend expects. **`USER_DATA`** β€” who the user is. Send after login, before the first call. ```kotlin EmbedSDK.event( EventKeys.USER_DATA, mapOf( "app_user_id" to "user_123", // your stable id β€” the important one "name" to "Jane Doe", "email" to "jane@email.com" ) ) ``` **`SCREEN_STATE`** β€” manual screen tracking, for hosts with no NavController. This enriches *events*; it does not drive widget visibility (use `currentScreen` / `setCurrentScreen` for that). ```kotlin EmbedSDK.event( EventKeys.SCREEN_STATE, mapOf("screen" to "ProductDetail", "action" to "enter") ) ``` **`ANALYTICS_DATA`** β€” analytics, keyed by `event_name`. This is also the key the SDK fires its own events on, which is what makes them observable. ```kotlin EmbedSDK.event( EventKeys.ANALYTICS_DATA, mapOf("event_name" to "checkout_started") ) ``` **`CUSTOM_EVENT`** β€” free-form host events. ### Listening β€” `on` / `off` [#listening--on--off] ```kotlin val cb: EventCallback = { data -> Log.d("Revrag", "data: $data") } EmbedSDK.on(EventKeys.ANALYTICS_DATA, cb) EmbedSDK.off(EventKeys.ANALYTICS_DATA, cb) // same instance, or it won't unhook ``` `off` matches on the **callback instance**. Hold it in a property β€” a lambda written inline at the `off` call site is a different object and removes nothing. ### Call lifecycle β€” `onAgent` / `offAgent` [#call-lifecycle--onagent--offagent] ```kotlin class MainActivity : AppCompatActivity() { private val onConnected: AgentEventCallback = { _ -> Log.d("Revrag", "agent call started") } private val onDisconnected: AgentEventCallback = { payload -> val duration = (payload["metadata"] as? Map<*, *>) ?.get("callDuration") as? Int ?: 0 Log.d("Revrag", "call lasted ${duration}s") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) EmbedSDK.onAgent(AgentEvent.AGENT_CONNECTED, onConnected) EmbedSDK.onAgent(AgentEvent.AGENT_DISCONNECTED, onDisconnected) } override fun onDestroy() { super.onDestroy() EmbedSDK.offAgent(AgentEvent.AGENT_CONNECTED, onConnected) EmbedSDK.offAgent(AgentEvent.AGENT_DISCONNECTED, onDisconnected) } } ``` `AgentEvent`: `AGENT_CONNECTED`, `AGENT_DISCONNECTED`, `POPUP_MESSAGE_VISIBLE`. This is the one place the "never write teardown" rule does not apply. The **widget** needs no teardown; a **listener** you registered does, or it holds a destroyed Activity for the life of the process. ### What the SDK fires on its own [#what-the-sdk-fires-on-its-own] All arrive on `ANALYTICS_DATA` with `event_name` set to one of: | `EmbedAnalyticsEvents.` | Fires when | | ----------------------------- | -------------------------------------- | | `AGENT_TAP_TO_OPEN` | the user taps the collapsed button | | `AGENT_TAP_TO_CLOSE` | the user collapses the card | | `AGENT_VISIBLE` | the button finishes fading in | | `AGENT_CONVERSATION_STARTED` | the call connects | | `AGENT_CONVERSATION_ENDED` | the call ends | | `AVATAR_MODE_OPENED` | the full-screen avatar opens | | `POPUP_MESSAGE_VISIBLE` | an inactivity nudge appears | | `GEN_TOOL_TRIGGERED` | the first data-channel message arrives | | `MICROPHONE_PERMISSION_ALLOW` | mic permission is granted | | `RAGE_CLICK` | rapid repeated taps are detected | | `FORM_EVENT` | a form interaction is tracked | | `ERROR` | the SDK hits an error | ```kotlin EmbedSDK.on(EventKeys.ANALYTICS_DATA) { data -> when (data["event_name"]) { EmbedAnalyticsEvents.AGENT_CONVERSATION_STARTED -> myAnalytics.track("call_start") EmbedAnalyticsEvents.ERROR -> Log.e("Revrag", "sdk error: $data") } } ``` Compare against the constants, never against string literals β€” the wire values are not part of the public contract. ### Context on every event [#context-on-every-event] ```kotlin EmbedSDK.setCurrentFlow("loan_application") // attached to every subsequent event EmbedSDK.setAppVersion("4.2.0") // only if yours differs from PackageInfo ``` ### Microphone permission, on demand [#microphone-permission-on-demand] ```kotlin EmbedSDK.checkPermissions(activity) { granted -> if (!granted) showYourOwnRationale() } ``` Optional β€” the SDK requests it at call start anyway. Use this to ask earlier, at a moment that makes sense in your flow. *** ## 6E. A drop-in event logger [#6e-a-drop-in-event-logger] Paste this once and every SDK event prints with a readable label. It is the fastest way to see the widget working, and the hook where you forward events to your own analytics. ```kotlin import ai.revrag.embed.android.EmbedAnalyticsEvents import ai.revrag.embed.android.EmbedSDK import ai.revrag.embed.android.EventCallback import ai.revrag.embed.android.EventKeys import android.util.Log object SdkLifecycleListener { private const val TAG = "SdkLifecycle" private var callback: EventCallback? = null /** * @param onEvent optional β€” forward every SDK event to your own analytics. */ fun register(onEvent: ((name: String, data: Map) -> Unit)? = null) { if (callback != null) return // idempotent val cb: EventCallback = { data -> val name = data["event_name"] as? String ?: "unknown" Log.d(TAG, "${label(name)} β†’ $data") onEvent?.invoke(name, data) } callback = cb EmbedSDK.on(EventKeys.ANALYTICS_DATA, cb) } fun unregister() { callback?.let { EmbedSDK.off(EventKeys.ANALYTICS_DATA, it) } callback = null } private fun label(name: String): String = when (name) { EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN -> "Widget expanded" EmbedAnalyticsEvents.AGENT_TAP_TO_CLOSE -> "Widget collapsed" EmbedAnalyticsEvents.AGENT_VISIBLE -> "Widget visible" EmbedAnalyticsEvents.AGENT_CONVERSATION_STARTED -> "Call started" EmbedAnalyticsEvents.AGENT_CONVERSATION_ENDED -> "Call ended" EmbedAnalyticsEvents.AVATAR_MODE_OPENED -> "Avatar opened" EmbedAnalyticsEvents.POPUP_MESSAGE_VISIBLE -> "Popup shown" EmbedAnalyticsEvents.MICROPHONE_PERMISSION_ALLOW -> "Mic permission granted" EmbedAnalyticsEvents.GEN_TOOL_TRIGGERED -> "Tool triggered" EmbedAnalyticsEvents.RAGE_CLICK -> "Rage click" EmbedAnalyticsEvents.FORM_EVENT -> "Form event" EmbedAnalyticsEvents.ERROR -> "SDK error" else -> name } } ``` ```kotlin // Application.onCreate, after initialize SdkLifecycleListener.register() // or forward to your analytics SdkLifecycleListener.register { name, data -> myAnalytics.track(name, data) } // on logout / teardown SdkLifecycleListener.unregister() ``` Because it holds the callback instance itself, `unregister()` actually unhooks β€” the mistake this helper exists to prevent. *** ## 7. How screens get their names [#7-how-screens-get-their-names] Four sources, highest wins: | | Source | You write | | - | ------------------------------------------------------------------ | --------------------------------- | | 1 | `attachOverlay(currentScreen = …)` or `handle.setCurrentScreen(…)` | a name, if you want prettier ones | | 2 | `NavController` destination `android:label` or route | pass the controller once | | 3 | Fragment class name | nothing | | 4 | **Activity class name** | nothing β€” the floor, never absent | Two rules worth knowing: * **Naming one screen by hand takes ownership.** Rung 3 stands down for the rest of the session. If you start naming, name them all. * **Names are matched exactly** and are case-sensitive. `"Home"` β‰  `"home"`. *** ## 8. Verify it works [#8-verify-it-works] Run with logcat filtered to `RevragEmbed`. Every visibility decision prints its inputs, so you can watch the config work screen by screen: ``` [Screen] SplashActivity#1 β†’ LoginActivity#2 [Visibility] 'SplashActivity' NOT allowed β€” hidden [Visibility] 'LoginActivity' group='onboarding' delayMs=500 waitMs=500 … [Visibility] 'LoginActivity' delay elapsed β€” showing [Visibility] 'Plans' same CONTINUOUS group 'main_tabs' β€” stay visible ``` | Filter | Shows | | -------------- | ------------------------------------------------- | | `[Screen]` | every screen change, and the name the SDK has | | `[Visibility]` | every show/hide decision and why | | `[Init]` | the handshake and whether a widget config arrived | **A good first run** shows `[Screen]` changing as you navigate, `NOT allowed` on your excluded screens, and `delay elapsed β€” showing` on your allowed ones. *** ## 9. Troubleshooting [#9-troubleshooting] ### The button never appears [#the-button-never-appears] Check these in order β€” the first two account for most cases. **1. Did a widget config arrive?** Filter logcat for `Init`: ``` [Init] widgetConfig: null backend returned NO widget_config for this API key β€” the widget cannot render. ``` That is provisioning, not your code: the key has no widget configured for that environment. Confirm your key and `embedUrl` are for the same environment, and ask Revrag to provision the key. **2. Does the name in the config match the name the SDK has?** Filter for `[Visibility]` and read the quoted name. A name in `allowedScreens` that no screen ever reports hides the widget silently. Case matters. **3. Is the screen excluded?** `excludedScreens` beats `allowedScreens`. **4. Did `attachOverlay` run after `setContentView()`?** From `onResume` it always does. A call in `onCreate` before `setContentView` is removed by it. **5. Is there an entrance delay still running?** `delayMs` is real β€” a 2500ms group delay looks like "not working" for two and a half seconds. ### The button disappears mid-flow [#the-button-disappears-mid-flow] Almost always teardown code. Remove any `detach()` in `onPause`/`onDestroy` and let the SDK own the hand-off. ### It appears on screens it should not [#it-appears-on-screens-it-should-not] An empty `allowedScreens` means **everywhere**. Either list your screens, or use `excludedScreens` for the ones to suppress. Not attaching on a screen is *not* a way to hide it β€” and it leaves a live call running. ### The call does not start [#the-call-does-not-start] `RECORD_AUDIO` missing from your manifest ([section 1](#1-install)). The SDK requests the runtime permission but cannot grant itself one you never declared. ### "Class was compiled with an incompatible version of Kotlin" [#class-was-compiled-with-an-incompatible-version-of-kotlin] ``` The binary version of its metadata is 2.0.0, expected version is 1.9.0 ``` Your project is on Kotlin 1.9.x. The SDK is compiled with 2.0.21 and its classes carry metadata 2.0, which older compilers refuse to read. Move your project to Kotlin 2.0 or newer; there is no flag that makes 1.9 accept it. Your own **Ktor** version is not part of this. The SDK's Ktor is relocated to `ai.revrag.shaded.ktor.*`, so it neither constrains nor conflicts with yours. ### Two widgets appear [#two-widgets-appear] Two mount mechanisms at once β€” usually `EmbedProviderComposable` *and* `attachOverlay`. Pick one. ### Something else [#something-else] Capture logcat filtered to `RevragEmbed` from app start and send it to Revrag with your screen names β€” it records every decision the SDK made. *** ## 10. Checklist [#10-checklist] * [ ] `implementation("ai.revrag:embed-android:1.1.0")` * [ ] `INTERNET` and `RECORD_AUDIO` in your manifest * [ ] `EmbedSDK.initialize(...)` in `Application.onCreate` * [ ] `Application` class registered in the manifest * [ ] One mount mechanism β€” `attachOverlay` from every resume, **or** `EmbedProviderComposable`, never both * [ ] `appUserId` set to your stable user id * [ ] Config names match what the SDK reports (Activity class names, nav labels, or Compose routes) * [ ] Splash and trampolines in `excludedScreens` β€” **not** simply un-attached * [ ] Screens that share a journey share a `CONTINUOUS` group * [ ] Nothing in `onPause` / `onDestroy` **for the widget itself** β€” but do deregister any `onAgent` / `onAvatarState` listeners you registered * [ ] `EmbedSDK.clearStorageCache()` on logout * [ ] `USER_DATA` sent with a valid `app_user_id` before the first call * [ ] Any `on` / `onAgent` listener is deregistered with the **same instance** * [ ] `defaultInset` set if a bottom bar or sticky CTA would sit under the button * [ ] Tested on a **physical device** β€” emulators mishandle microphone and audio routing * [ ] Verified in logcat: `[Visibility] … delay elapsed β€” showing` on an allowed screen, `NOT allowed β€” hidden` on an excluded one *** ## 11. Upgrading [#11-upgrading] ### 1.0.8 β†’ 1.1.0 [#108--110] Drop-in: no code changes required. What you gain: * **The SDK follows Activity changes on its own** (`autoTrackActivities`, default on). Multi-Activity apps no longer need to name every Activity β€” see [5A](#5a--xml-one-activity-per-screen). Your existing explicit names still win. * **Automatic screen names** from Activity class and fragment class, so `allowedScreens` works on screens you never wired up. * Widget lifecycle fixes across Activity hand-overs, dialog windows and configuration changes. If you were relying on *not* calling `attachOverlay` on a screen as a way to hide the widget, that no longer hides it β€” the SDK finds the Activity by itself. Move those screens to `excludedScreens`, which is better anyway: it also ends a live call, which non-attachment never did. ### 1.0.7 β†’ 1.0.8 [#107--108] Adds call-control APIs, widget control and `EmbedAnalyticsEvents`. No changes required. ### 1.0.6 β†’ 1.0.7 [#106--107] Fixes Ktor class conflicts by shading. No changes required. *** ## Reference [#reference] | | | | ------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Artifact | `ai.revrag:embed-android:1.1.0` | | Maven Central | [https://central.sonatype.com/artifact/ai.revrag/embed-android](https://central.sonatype.com/artifact/ai.revrag/embed-android) | *** ## Support [#support] * Issues: [GitHub Issues](https://github.com/revrag-ai/embed-android/issues) * Docs: [Revrag Documentation](https://docs.revrag.ai) * Dashboard: [app.revrag.ai](https://app.revrag.ai) --- # Angular > Angular SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities URL: /embed/integration/angular Markdown: /embed/integration/angular.md # πŸš€ Complete Integration Guide [#-complete-integration-guide] ## πŸ“¦ Installation [#-installation] ```bash npm install @revrag-ai/embed-angular livekit-client lottie-web # or yarn add @revrag-ai/embed-angular livekit-client lottie-web ``` ### Peer Dependencies [#peer-dependencies] Ensure the following Angular packages are present in your project (they are usually already installed): | Package | Version | | --------------------- | ---------- | | `@angular/core` | `>=15.0.0` | | `@angular/common` | `>=15.0.0` | | `@angular/animations` | `>=15.0.0` | | `@angular/router` | `>=15.0.0` | | `livekit-client` | `^2.0.0` | | `lottie-web` | `^5.10.0` | ## ⚠️ Important: CSS Import (REQUIRED) [#️-important-css-import-required] **The CSS file MUST be imported** for the widget to display correctly. Without it, the widget will appear unstyled. ### Option 1: angular.json / project.json (Recommended) [#option-1-angularjson--projectjson-recommended] Add the path to the `styles` array in your `angular.json` (or `project.json` for Nx workspaces): ```json { "projects": { "your-app": { "architect": { "build": { "options": { "styles": [ "src/styles.css", "node_modules/@revrag-ai/embed-angular/styles/widget.css" ] } } } } } } ``` ### Option 2: Global styles.css Import [#option-2-global-stylescss-import] ```css /* In your global styles.css */ @import '@revrag-ai/embed-angular/styles/widget.css'; ``` ### Option 3: Component-Level Import [#option-3-component-level-import] ```css /* In a component's .css / .scss file */ @import '@revrag-ai/embed-angular/styles/widget.css'; ``` *** ## ⚑ Setup [#-setup] Three one-time steps are required before placing any widget component. ### Step 1 β€” Provide Animations [#step-1--provide-animations] Angular animations must be enabled at the application level. Without this the widget transitions will not work. **Standalone bootstrap (`app.config.ts`):** ```typescript import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideAnimations } from '@angular/platform-browser/animations'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideAnimations(), // required ], }; ``` **NgModule bootstrap (`app.module.ts`):** ```typescript import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; @NgModule({ imports: [BrowserModule, BrowserAnimationsModule], // BrowserAnimationsModule required }) export class AppModule {} ``` ### Step 2 β€” Initialize the SDK [#step-2--initialize-the-sdk] Call `EmbedInitService.initialize()` **once**, as early as possible β€” in your root `AppComponent`. It validates your API key, fetches widget configuration from Revrag's backend, and caches the result for the session. ```typescript // app.component.ts import { Component, OnInit } from '@angular/core'; import { RouterModule } from '@angular/router'; import { EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-root', standalone: true, imports: [RouterModule], template: ``, }) export class AppComponent implements OnInit { constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.initialize('your-api-key'); } } ``` Get your API key from the [Revrag dashboard](https://revrag.ai). *** ## 🎯 Basic Usage [#-basic-usage] ### Fixed Positioning (Default) [#fixed-positioning-default] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-my-page', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

My Application

`, }) export class MyPageComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } } ``` ### Embedded Positioning [#embedded-positioning] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-help-section', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

Need Help?

Chat with our AI assistant

`, }) export class HelpSectionComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } } ``` *** ## 🎨 Responsive Behavior [#-responsive-behavior] ### Fixed Positioning [#fixed-positioning] * **Desktop (> 500px)**: Widget stays at specified position * **Mobile (≀ 500px)**: Widget expands to full width with 1rem padding from edges ### Embedded Positioning [#embedded-positioning-1] * **Wide parent (> 400px)**: Widget aligns to left/right based on `side` input * **Narrow parent (≀ 400px)**: Widget auto-centers with equal padding *** ## πŸ”§ API Reference [#-api-reference] ### EmbedButtonComponent Inputs [#embedbuttoncomponent-inputs] **Selector:** `revrag-embed-button` ```typescript interface EmbedButtonInputs { // Positioning mode positioning?: 'fixed' | 'embedded'; // default: 'fixed' // Position configuration (for fixed mode) position?: { top?: string; bottom?: string; left?: string; right?: string; zIndex?: number; }; // Easy positioning (for embedded mode) side?: 'left' | 'right' | 'center'; // Offset from bottom (useful for bottom navbars) bottomOffset?: number; // in pixels // Custom CSS class className?: string; } ``` ### EmbedProviderComponent Inputs [#embedprovidercomponent-inputs] **Selector:** `revrag-embed-provider` Use this component to declaratively control widget visibility based on the current route. ```typescript interface EmbedProviderInputs { // Active URL path β€” keep in sync with router currentPath?: string; // Paths where the widget should be visible includeScreens?: string[]; // How paths are matched matchMode?: 'exact' | 'startsWith'; // default: 'exact' // Delay before button appears (ms) embedButtonDelayMs?: number; // Advanced group-based visibility rules embedButtonVisibilityConfig?: EmbedButtonVisibilityConfig; // Override button position embedButtonPosition?: EmbedButtonPosition; // Widget positioning mode widgetPositioning?: 'fixed' | 'embedded'; // default: 'fixed' // Horizontal alignment widgetSide?: 'left' | 'right'; // Extra pixels from bottom edge widgetBottomOffset?: number; // Extra CSS class on widget container widgetClassName?: string; } ``` Advanced usage guide β†’ ### EmbedInitService [#embedinitservice] ```typescript import { EmbedInitService } from '@revrag-ai/embed-angular'; constructor(private embedInit: EmbedInitService) {} // Initialize the SDK (call once in AppComponent) await this.embedInit.initialize(apiKey: string, options?: { baseUrl?: string; // Override API base URL enableTracker?: boolean; // Enable automatic tracking trackerCallback?: (event: EventPayload) => void; }); // Observables this.embedInit.isInitialized$ // Observable this.embedInit.isLoading$ // Observable this.embedInit.error$ // Observable this.embedInit.sessionData$ // Observable // Synchronous getter this.embedInit.isInitialized // boolean ``` *** ## πŸ“‘ Event Management [#-event-management] The SDK provides a powerful event system for tracking user data, custom events, and listening to agent state changes. Use the `embedEvent` singleton (re-exported from `@revrag-ai/embed-angular`) β€” it has the same API as `embed` in the React SDK. ### EventKeys [#eventkeys] Available event types: ```typescript import { EventKeys } from '@revrag-ai/embed-angular'; EventKeys.USER_DATA // 'user_data' - User identification and profile data EventKeys.CUSTOM_EVENT // 'custom_event' - Custom application events EventKeys.AGENT_CONNECTED // 'agent_start' - Voice agent connection (auto-tracked) EventKeys.AGENT_DISCONNECTED // 'agent_end' - Voice agent disconnection (auto-tracked) EventKeys.ANALYTICS_DATA // 'analytics_data' ``` **Note**: Only `USER_DATA`, `CUSTOM_EVENT` and `ANALYTICS_DATA` are available for manual use. Agent connection events are automatically tracked by the SDK and can be listened to via callbacks. *** ### Sending Events with embedEvent API [#sending-events-with-embedevent-api] The `embedEvent` object provides methods for sending events to track user data and custom application events. #### Send User Data [#send-user-data] ```typescript import { embedEvent, EventKeys } from '@revrag-ai/embed-angular'; // Send user data const response = await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'user@example.com', name: 'John Doe', plan: 'premium' } }); if (response.success) { console.log('User data sent successfully'); } ``` #### Send Custom Events [#send-custom-events] ```typescript import { embedEvent, EventKeys } from '@revrag-ai/embed-angular'; // Track custom application event await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', product_id: 'prod-123', amount: 99.99 } }); ``` #### Send Analytics Events [#send-analytics-events] ```typescript import { embedEvent, EventKeys } from '@revrag-ai/embed-angular'; // Track analytics event await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, data: { event_name: 'purchase_completed', // event_name is compulsory in analytics_data event product_id: 'prod-123', amount: 99.99 } }); ``` #### Event Method Signature [#event-method-signature] ```typescript embedEvent.event(params: UpdateDataRequest): Promise interface UpdateDataRequest { eventKey: EventKey; // Event type from EventKeys data: { app_user_id?: string; // User ID (required for USER_DATA) [key: string]: unknown; // Additional data }; session_id?: string; // Optional session ID } interface ApiResponse { success: boolean; data?: unknown; message?: string; error?: string; } ``` *** ### Listening to Agent Events [#listening-to-agent-events] Monitor voice agent connection status in real-time. **These events are automatically sent to your backend AND emitted locally** for you to listen to. #### Available Event Types for Listening [#available-event-types-for-listening] ```typescript import { EventKeys } from '@revrag-ai/embed-angular'; // Available events for listening: EventKeys.AGENT_CONNECTED // 'agent_start' - Voice agent connected EventKeys.AGENT_DISCONNECTED // 'agent_end' - Voice agent disconnected ``` **Automatic Backend Sync:** * Agent events are **automatically sent to your backend** with `app_user_id` * Events are **also emitted locally** for real-time UI updates * Backend receives all event data including timestamps and metadata * No manual API calls needed β€” it's all handled automatically #### Event Listener Methods [#event-listener-methods] ```typescript import { embedEvent } from '@revrag-ai/embed-angular'; // Add event listener embedEvent.addCallback(callback); // Remove event listener embedEvent.removeCallback(callback); ``` #### Basic Event Listening Example [#basic-event-listening-example] ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { embedEvent, EventKeys, EmbedButtonComponent } from '@revrag-ai/embed-angular'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-my-component', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: ``, }) export class MyComponent implements OnInit, OnDestroy { private handleAgentEvent = (event: any) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('βœ… Agent connected:', event.data); // Update UI to show agent is available } if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('❌ Agent disconnected:', event.data); // Update UI to show agent is unavailable } }; ngOnInit(): void { embedEvent.addCallback(this.handleAgentEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleAgentEvent); } } ``` #### Complete Agent Monitoring Example [#complete-agent-monitoring-example] ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { embedEvent, EventKeys, EmbedButtonComponent } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-voice-agent-monitor', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `
Agent Status: {{ agentStatus }}

βœ“ Voice agent is active

Identity: {{ agentIdentity }}

Connected at: {{ connectionTime | date:'mediumTime' }}

`, }) export class VoiceAgentMonitorComponent implements OnInit, OnDestroy { agentStatus: 'idle' | 'connected' | 'disconnected' = 'idle'; agentIdentity = ''; connectionTime: Date | null = null; private handleEvent = (event: any) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('βœ… Agent connected:', event); console.log('Identity:', event.data?.identity); console.log('Metadata:', event.data?.metadata); console.log('Timestamp:', event.timestamp); this.agentStatus = 'connected'; this.agentIdentity = event.data?.identity || 'Unknown'; this.connectionTime = new Date(event.timestamp); } if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('❌ Agent disconnected:', event); this.agentStatus = 'disconnected'; if (this.connectionTime) { const duration = Date.now() - this.connectionTime.getTime(); console.log('Call duration:', duration / 1000, 'seconds'); } } }; ngOnInit(): void { embedEvent.addCallback(this.handleEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleEvent); } } ``` #### Use Cases for Agent Events [#use-cases-for-agent-events] **AGENT\_CONNECTED:** * Show visual indicators (green dot, badge) * Enable voice-related features in UI * Start analytics timers * Update user presence status * Show notifications to user * Pause background music/media **AGENT\_DISCONNECTED:** * Update UI to show agent unavailable * Log analytics (call duration, success) * Show feedback forms * Resume background media * Clean up resources * Save conversation state #### Handling Connection Errors [#handling-connection-errors] ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { embedEvent, EventKeys, EmbedButtonComponent } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-agent-with-error-handling', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `
{{ error }}
`, }) export class AgentWithErrorHandlingComponent implements OnInit, OnDestroy { error: string | null = null; private handleEvent = (event: any) => { try { if (event.type === EventKeys.AGENT_CONNECTED) { this.error = null; } if (event.type === EventKeys.AGENT_DISCONNECTED) { if (event.data?.metadata?.error) { this.error = 'Agent connection lost: ' + event.data.metadata.error; } } } catch (err) { console.error('Error handling agent event:', err); this.error = 'Failed to process agent event'; } }; ngOnInit(): void { embedEvent.addCallback(this.handleEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleEvent); } } ``` *** ## πŸ“± Mobile Optimization [#-mobile-optimization] The widget automatically adjusts for mobile devices: ### Extra Small Screens (≀ 375px) [#extra-small-screens--375px] * iPhone SE, small Android devices * Reduced padding and font sizes * Optimized button and text layouts ### Small Screens (376px - 500px) [#small-screens-376px---500px] * Standard smartphones * Balanced sizing for readability *** ## 🎯 Common Use Cases [#-common-use-cases] ### 1. Customer Support Widget with User Tracking [#1-customer-support-widget-with-user-tracking] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { EmbedButtonComponent, EmbedInitService, embedEvent, EventKeys, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterModule, EmbedButtonComponent], template: ` `, }) export class AppComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.initialize('your-api-key').then(() => { this.isInitialized = true; this.sendUserData(); }); } private async sendUserData(): Promise { const currentUser = this.getCurrentUser(); // your auth method if (currentUser) { await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: currentUser.id, email: currentUser.email, name: currentUser.name, subscription_tier: currentUser.plan, }, }); } } private getCurrentUser() { // replace with your auth service call return null; } } ``` ### 2. E-commerce with Purchase Tracking [#2-e-commerce-with-purchase-tracking] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService, embedEvent, EventKeys, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-checkout', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

Checkout

`, }) export class CheckoutPageComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } async handleCheckout(): Promise { // ... process order await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', order_id: 'order-456', total: 99.99, items: 3, }, }); } } ``` ### 3. Help Section Widget with Agent Status [#3-help-section-widget-with-agent-status] ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService, embedEvent, EventKeys, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-help-page', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

Help & Support

🟒 Agent Connected
`, }) export class HelpPageComponent implements OnInit, OnDestroy { isInitialized = false; agentConnected = false; private handleAgentEvent = (event: any) => { if (event.type === EventKeys.AGENT_CONNECTED) { this.agentConnected = true; } if (event.type === EventKeys.AGENT_DISCONNECTED) { this.agentConnected = false; } }; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); embedEvent.addCallback(this.handleAgentEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleAgentEvent); } } ``` ### 4. With Bottom Navigation [#4-with-bottom-navigation] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-mobile-shell', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `
`, }) export class MobileShellComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } } ``` ### 5. Multi-Department Support [#5-multi-department-support] ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-support-page', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

Sales Support

Questions about pricing and plans

Technical Support

Help with technical issues

`, }) export class SupportPageComponent implements OnInit { isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } } ``` ### 6. Contextual Events Based on User Actions [#6-contextual-events-based-on-user-actions] ```typescript import { Component, Input, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedButtonComponent, EmbedInitService, embedEvent, EventKeys, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-product-page', standalone: true, imports: [CommonModule, EmbedButtonComponent], template: `

{{ product.name }}

{{ product.price | currency }}

`, }) export class ProductPageComponent implements OnInit { @Input() product: { id: string; name: string; price: number } = { id: '', name: '', price: 0 }; isInitialized = false; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); } async handleAddToCart(): Promise { await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'product_added_to_cart', product_id: this.product.id, product_name: this.product.name, price: this.product.price, }, }); } } ``` *** ## πŸ”„ Complete Integration Example [#-complete-integration-example] Here's a complete example showing initialization, user tracking, event listening, and the widget all working together: ```typescript // app.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { EmbedButtonComponent, EmbedInitService, embedEvent, EventKeys, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterModule, EmbedButtonComponent], template: `

My Application

Agent Status: {{ agentStatus }}
`, }) export class AppComponent implements OnInit, OnDestroy { isInitialized = false; isLoading = false; error: string | null = null; agentStatus: 'idle' | 'connected' | 'disconnected' = 'idle'; userDataSent = false; private handleAgentEvent = (event: any) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('Agent connected:', event); this.agentStatus = 'connected'; } if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('Agent disconnected:', event); this.agentStatus = 'disconnected'; } }; constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { // 1. Subscribe to loading and error states this.embedInit.isLoading$.subscribe((loading) => { this.isLoading = loading; }); this.embedInit.error$.subscribe((err) => { this.error = err; }); // 2. Initialize SDK this.embedInit.initialize('your-api-key').then(() => { this.isInitialized = true; this.initializeUserData(); }); // 3. Listen to agent events embedEvent.addCallback(this.handleAgentEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleAgentEvent); } private async initializeUserData(): Promise { try { await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'user@example.com', name: 'John Doe', subscription: 'premium', }, }); this.userDataSent = true; } catch (err) { console.error('Failed to initialize user data:', err); } } async handlePurchase(): Promise { await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', amount: 99.99, product_id: 'prod-123', }, }); } } ``` ### Key Points in This Example: [#key-points-in-this-example] 1. **βœ… CSS Import**: Added to `angular.json` styles array 2. **βœ… Animations**: `provideAnimations()` in `app.config.ts` 3. **βœ… SDK Initialization**: `EmbedInitService.initialize()` in `ngOnInit()` with loading/error states 4. **βœ… User Data**: Sent first before rendering the widget 5. **βœ… Event Listeners**: Registered in `ngOnInit()`, cleaned up in `ngOnDestroy()` 6. **βœ… Custom Events**: Tracked when user performs actions 7. **βœ… Widget Rendering**: Only rendered after successful initialization and user data sent *** ## ⚑ Angular-Specific Notes [#-angular-specific-notes] ### Standalone Components (Recommended, Angular 15+) [#standalone-components-recommended-angular-15] Import `EmbedButtonComponent` or `EmbedProviderComponent` directly into each component's `imports` array: ```typescript @Component({ standalone: true, imports: [CommonModule, EmbedButtonComponent], // ... }) export class MyComponent {} ``` ### NgModule-based Apps [#ngmodule-based-apps] For applications that have not migrated to standalone components, import `EmbedModule` once in your root or feature module: ```typescript // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { EmbedModule } from '@revrag-ai/embed-angular'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, // required EmbedModule, // all selectors available everywhere ], bootstrap: [AppComponent], }) export class AppModule {} ``` All component selectors and inputs remain the same. You do **not** need to import individual components when `EmbedModule` is imported. ### Server-Side Rendering (SSR / Angular Universal) [#server-side-rendering-ssr--angular-universal] The SDK uses `PlatformService` internally to detect the browser platform and skips all DOM operations during SSR. No additional configuration is needed β€” the widget renders nothing on the server and hydrates cleanly on the client. ### Tailwind CSS Projects [#tailwind-css-projects] βœ… No conflicts! The library uses custom `embed-*` prefixed classes βœ… Your Tailwind styles won't affect the widget βœ… Widget styles won't affect your app *** ## πŸ› Troubleshooting [#-troubleshooting] ### Widget appears unstyled [#widget-appears-unstyled] **Solution**: Make sure the CSS path is in your `angular.json` styles array: ```json "styles": [ "src/styles.css", "node_modules/@revrag-ai/embed-angular/styles/widget.css" ] ``` Rebuild the app after making this change. ### Widget not appearing [#widget-not-appearing] **Solution**: Ensure `EmbedInitService.initialize()` has completed before rendering the widget: ```typescript this.embedInit.isInitialized$.subscribe((ready) => { this.isInitialized = ready; }); ``` Check the browser console for initialization errors β€” your API key may be invalid. ### Widget overlaps with bottom navigation [#widget-overlaps-with-bottom-navigation] **Solution**: Use the `bottomOffset` input: ```html ``` ### Animations are broken or missing [#animations-are-broken-or-missing] **Solution**: Ensure `provideAnimations()` (standalone) or `BrowserAnimationsModule` (NgModule) is present in your app config. ### Events not being sent [#events-not-being-sent] **Solution**: Ensure you've sent `USER_DATA` event first with `app_user_id`: ```typescript await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', // ... other data } }); ``` ### Agent event listeners not firing [#agent-event-listeners-not-firing] **Solution**: Make sure callbacks are registered in `ngOnInit()` before the agent connects, and cleaned up in `ngOnDestroy()`: ```typescript ngOnInit(): void { embedEvent.addCallback(this.handleEvent); } ngOnDestroy(): void { embedEvent.removeCallback(this.handleEvent); } ``` ### "User identity not found" error [#user-identity-not-found-error] **Solution**: Send `USER_DATA` event before any other events: ```typescript // βœ… Correct order await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123' } }); await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { ... } }); // ❌ Wrong order await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { ... } }); // Error! ``` ### Custom events being blocked [#custom-events-being-blocked] **Solution**: Only `USER_DATA` and `CUSTOM_EVENT` are allowed for manual sending. Agent events (`AGENT_CONNECTED`, `AGENT_DISCONNECTED`) are auto-tracked and can only be listened to, not manually sent. ### Analytics event missing or rejected [#analytics-event-missing-or-rejected] **Solution**: `event_name` is a **required** field in every `ANALYTICS_DATA` event. Omitting it will cause the event to be dropped silently. ```typescript // ❌ Missing event_name β€” event will be rejected await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, data: { product_id: 'prod-456', value: 299, } }); // βœ… Correct β€” event_name is always required await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, data: { event_name: 'purchase_completed', // required product_id: 'prod-456', value: 299, } }); ``` ### Widget appears behind other elements [#widget-appears-behind-other-elements] **Solution**: Use the `position` input to set a custom `zIndex`: ```html ``` *** ## πŸ“‹ Checklist [#-checklist] Before deploying, ensure: **Basic Setup:** * [ ] CSS path is in `angular.json` styles array * [ ] `provideAnimations()` or `BrowserAnimationsModule` is configured * [ ] API key is configured * [ ] `isInitialized$` is checked before rendering the widget * [ ] Parent container has `position: relative` (for embedded mode) * [ ] Parent container has sufficient height (for embedded mode) * [ ] Bottom offset is set if you have bottom navigation **Event System:** * [ ] `USER_DATA` event sent first with `app_user_id` * [ ] `USER_DATA` sent before rendering `revrag-embed-button` * [ ] Event listeners registered in `ngOnInit()` before agent connection * [ ] Event listeners cleaned up in `ngOnDestroy()` * [ ] Custom events include proper context (screen, flow) **Production Readiness:** * [ ] Error handling for failed event sends * [ ] Loading states during SDK initialization (`isLoading$`) * [ ] Agent connection status displayed to users * [ ] Analytics tracking for agent events * [ ] Proper cleanup of callbacks on component destroy *** ## πŸ†˜ Support [#-support] * πŸ“§ Issues: [GitHub Issues](https://github.com/revrag-ai/embed-react/issues) * πŸ“– Docs: [GitHub README](https://github.com/revrag-ai/embed-react) * πŸ’¬ Discussions: [GitHub Discussions](https://github.com/revrag-ai/embed-react/discussions) *** ## πŸŽ‰ You're All Set! [#-youre-all-set] The widget is now ready to use. It's: * βœ… Fully responsive * βœ… Angular-native (standalone + NgModule) * βœ… Tailwind-compatible * βœ… Production-ready * βœ… Mobile-optimized * βœ… Real-time event tracking * βœ… Voice agent monitoring * βœ… User context aware * βœ… SSR-safe (Angular Universal) ### Quick Reference [#quick-reference] **Import everything you need:** ```typescript import { EmbedButtonComponent, // The main widget component selector: revrag-embed-button EmbedProviderComponent, // Route-aware wrapper selector: revrag-embed-provider EmbedInitService, // SDK initialization service embedEvent, // Event management API EventKeys, // Event type constants EmbedModule, // NgModule bundle (NgModule apps only) } from '@revrag-ai/embed-angular'; ``` **Initialize and track:** ```typescript // 1. Initialize SDK (in AppComponent.ngOnInit) await this.embedInit.initialize('your-api-key'); // 2. Send user data await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123' } }); // 3. Listen to agent events (register in ngOnInit, remove in ngOnDestroy) embedEvent.addCallback((event) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('Agent connected!'); } }); // 4. Render widget // ``` Happy coding! πŸš€ Built with [Mintlify](https://mintlify.com). --- # EmbedProvider advanced (Angular) > Advanced EmbedProviderComponent patterns for Angular: route visibility, delay policies, group configuration, programmatic control, and best practices. URL: /embed/integration/embed-angular-provider-advanced Markdown: /embed/integration/embed-angular-provider-advanced.md # Angular β€” Advanced Provider Usage [#angular--advanced-provider-usage] > Advanced `EmbedProviderComponent` patterns β€” route-based visibility, delay policies, group configuration, programmatic control, and best practices. *** ## Table of Contents [#table-of-contents] 1. [Quick Start](#quick-start) 2. [EmbedProviderComponent β€” Props Reference](#embedprovidercomponent--props-reference) 3. [embedButtonVisibilityConfig](#embedbuttonvisibilityconfig) 4. [EmbedButtonComponent Props](#embedbuttoncomponent-props) 5. [EmbedInitService](#embedinitservice) 6. [Usage Patterns](#usage-patterns) 7. [Best Practices](#best-practices) *** ## Quick Start [#quick-start] ```typescript // app.component.ts import { Component, OnInit } from '@angular/core'; import { RouterModule } from '@angular/router'; import { EmbedInitService } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-root', standalone: true, imports: [RouterModule], template: ``, }) export class AppComponent implements OnInit { constructor(private embedInit: EmbedInitService) {} ngOnInit(): void { this.embedInit.initialize('your_api_key'); } } ``` ```typescript // app-shell.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs'; import { EmbedProviderComponent } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, RouterModule, EmbedProviderComponent], template: ` `, }) export class AppShellComponent { currentPath = '/'; constructor(private router: Router) { this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => (this.currentPath = e.urlAfterRedirects)); } } ``` **Required steps:** 1. Call `EmbedInitService.initialize(apiKey)` in `AppComponent.ngOnInit()`. 2. Wrap your layout with ``. 3. Keep `currentPath` in sync with the Angular router. 4. Add the CSS to `angular.json` styles array. *** ## EmbedProviderComponent β€” Props Reference [#embedprovidercomponent--props-reference] **Selector:** `revrag-embed-provider` | Input | Type | Default | Description | | ----------------------------- | ------------------------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `currentPath` | `string \| undefined` | `undefined` | **Highest-priority** path override. Keep in sync with the Angular router (or active tab). When omitted the widget is always hidden. | | `includeScreens` | `string[]` | `[]` | Paths where the embed button is visible. Empty = never shown via provider (use `EmbedButtonComponent` directly). | | `matchMode` | `'exact' \| 'startsWith'` | `'exact'` | `exact` β€” path must match exactly. `startsWith` β€” path and all sub-routes (e.g. `/help` matches `/help/faq`). | | `embedButtonDelayMs` | `number` | `0` | Global delay (ms) before showing the button. Applies when no group config matches. | | `embedButtonVisibilityConfig` | `EmbedButtonVisibilityConfig \| undefined` | `undefined` | Per-group config for delays and continuity. See below. | | `embedButtonPosition` | `EmbedButtonPosition \| undefined` | `undefined` | Override the button's pixel position (`bottom`, `right`). | | `widgetPositioning` | `'fixed' \| 'embedded'` | `'fixed'` | `fixed` β€” viewport-fixed. `embedded` β€” in normal document flow. | | `widgetSide` | `'left' \| 'right' \| undefined` | `undefined` | Horizontal alignment of the widget. | | `widgetBottomOffset` | `number` | `0` | Extra pixels to raise the widget from the bottom. | | `widgetClassName` | `string` | `''` | Extra CSS class applied to the widget container. | ### Path detection [#path-detection] Unlike React's `EmbedProvider` (which auto-detects `window.location.pathname`), Angular's `EmbedProviderComponent` requires you to supply `currentPath` explicitly because Angular's router is service-based. Always keep it in sync with `NavigationEnd` events: ```typescript this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => { this.currentPath = e.urlAfterRedirects; }); ``` *** ## embedButtonVisibilityConfig [#embedbuttonvisibilityconfig] Use this for per-route or per-group behavior β€” different delays, policies, and continuity settings. ```typescript import { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-angular'; visibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1500, groups: [ { id: 'perScreen', screens: ['/offers'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'perScreen', }, { id: 'oncePerGroup', screens: ['/checkout', '/payment'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'oncePerGroupEntry', }, { id: 'oncePerSession', screens: ['/support'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'oncePerAppSession', }, ], }; ``` ```html ``` ### EmbedButtonVisibilityConfig [#embedbuttonvisibilityconfig-1] | Field | Type | Description | | ---------------- | -------------------------- | --------------------------------------------------------- | | `defaultDelayMs` | `number` | Fallback delay when a group matches but has no `delayMs`. | | `groups` | `EmbedButtonGroupConfig[]` | Per-group configuration array. | ### EmbedButtonGroupConfig [#embedbuttongroupconfig] | Field | Type | Description | | ------------- | ----------------------------- | ------------------------------------------------------------- | | `id` | `string` | Unique ID for the group (used for session/continuity logic). | | `screens` | `string[]` | Paths belonging to this group. | | `continuity` | `'perScreen' \| 'continuous'` | See EmbedButtonContinuity below. | | `delayMs` | `number` | Delay (ms) before showing the button when this group matches. | | `delayPolicy` | `EmbedButtonDelayPolicy` | When to apply the delay. See below. | ### EmbedButtonContinuity [#embedbuttoncontinuity] | Value | Behavior | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `'perScreen'` | Treat each screen in the group as a separate visit. Delays apply per screen according to `delayPolicy`. | | `'continuous'` | Once you enter the group, the button stays visible while navigating within the group. No delay on subsequent screens in the same group. | ### EmbedButtonDelayPolicy [#embedbuttondelaypolicy] | Value | When delay applies | | --------------------- | ------------------------------------------------------------------------------------------------------- | | `'perScreen'` | Every time you land on any screen in the group. | | `'oncePerGroupEntry'` | Only when first entering the group from outside. No delay when moving between screens within the group. | | `'oncePerAppSession'` | Only the first time you visit any screen in this group during the session. Page refresh resets. | *** ## EmbedButtonComponent Props [#embedbuttoncomponent-props] Props for the standalone `` component (also forwarded internally by `EmbedProviderComponent`). | Input | Type | Default | Description | | -------------- | -------------------------------------------- | ----------- | ------------------------------------------------------------- | | `positioning` | `'fixed' \| 'embedded'` | `'fixed'` | `fixed` β€” viewport-fixed. `embedded` β€” in-flow. | | `side` | `'left' \| 'right' \| 'center' \| undefined` | `undefined` | Auto-position in bottom-left, bottom-right, or bottom-center. | | `bottomOffset` | `number` | `0` | Extra offset from bottom (e.g. for nav bars). | | `position` | `PositionConfig \| undefined` | `undefined` | Fine-grained CSS position override. | | `className` | `string` | `''` | Additional CSS class for the button container. | ### PositionConfig [#positionconfig] ```typescript interface PositionConfig { bottom?: string; // e.g. '24px' right?: string; // e.g. '24px' left?: string; top?: string; transform?: string; zIndex?: number; } ``` ### Example [#example] ```html ``` *** ## EmbedInitService [#embedinitservice] Initializes the SDK. Call **once** in `AppComponent.ngOnInit()` before any widget renders. ```typescript import { EmbedInitService } from '@revrag-ai/embed-angular'; constructor(private embedInit: EmbedInitService) {} // Initialize await this.embedInit.initialize('your_api_key', { baseUrl: 'https://custom.api.example.com', // optional enableTracker: true, // optional trackerCallback: (event) => { ... }, // optional }); ``` ### SDKConfig options [#sdkconfig-options] | Field | Type | Description | | ----------------- | ------------------------------- | ------------------------------------ | | `baseUrl` | `string` | Override API base URL. | | `enableTracker` | `boolean` | Enable analytics/tracking. | | `trackerCallback` | `(event: EventPayload) => void` | Custom callback for tracking events. | ### Return / Observables [#return--observables] | Observable / Getter | Type | Description | | ------------------- | -------------------------------- | ---------------------------------------------------- | | `isInitialized$` | `Observable` | Emits `true` after successful init. | | `isLoading$` | `Observable` | Emits `true` while init is in progress. | | `error$` | `Observable` | Emits error message if init fails, `null` otherwise. | | `sessionData$` | `Observable` | Stored session/config for debugging. | | `isInitialized` | `boolean` (getter) | Synchronous check. | *** ## Usage Patterns [#usage-patterns] ### Pattern 1 β€” Manual conditional rendering [#pattern-1--manual-conditional-rendering] **Best for:** apps where you want explicit, imperative control over when the widget appears. Use `*ngIf` to conditionally render `EmbedButtonComponent` based on the current route. ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs'; import { EmbedButtonComponent } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, RouterModule, EmbedButtonComponent], template: ` `, }) export class AppShellComponent { showEmbed = false; constructor(private router: Router) { this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => { this.showEmbed = e.urlAfterRedirects === '/offers'; }); } } ``` **Pros:** Simple, explicit, no extra context. **Cons:** Pathnames are hardcoded next to `*ngIf`; adding/removing screens means editing the same conditional. *** ### Pattern 2 β€” EmbedProvider with `includeScreens` [#pattern-2--embedprovider-with-includescreens] **Best for:** router-based apps where the widget should appear on a known set of routes. The provider handles route matching, delay logic, and button lifecycle automatically. ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs'; import { EmbedProviderComponent } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, RouterModule, EmbedProviderComponent], template: ` `, }) export class AppShellComponent { currentPath = '/'; constructor(private router: Router) { this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => (this.currentPath = e.urlAfterRedirects)); } } ``` To show the widget on `/help` **and** all sub-routes like `/help/faq`, use `matchMode="startsWith"`: ```html ``` *** ### Pattern 3 β€” Object-based config [#pattern-3--object-based-config] **Best for:** teams that prefer keeping widget configuration in a separate object (easier to share across components, load from a config service, or drive from environment variables). ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs'; import { EmbedProviderComponent } from '@revrag-ai/embed-angular'; const EMBED_CONFIG = { includeScreens: ['/offers', '/help', '/support'], matchMode: 'exact' as const, }; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, RouterModule, EmbedProviderComponent], template: ` `, }) export class AppShellComponent { currentPath = '/'; embedConfig = EMBED_CONFIG; constructor(private router: Router) { this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => (this.currentPath = e.urlAfterRedirects)); } } ``` #### Config from a service or environment [#config-from-a-service-or-environment] ```typescript // embed.config.ts export const EMBED_CONFIG = { includeScreens: (import.meta.env['VITE_EMBED_SCREENS'] ?? '/offers,/help').split(','), matchMode: 'exact' as const, }; ``` For Angular CLI apps with `environment.ts`: ```typescript // environments/environment.ts export const environment = { embedApiKey: 'your_api_key', embedScreens: ['/offers', '/help', '/support'], }; ``` ```typescript // app.component.ts import { environment } from '../environments/environment'; ngOnInit(): void { this.embedInit.initialize(environment.embedApiKey); } ``` ```html ``` ```typescript embedScreens = environment.embedScreens; ``` *** ### Pattern 4 β€” Tab-based navigation (no router) [#pattern-4--tab-based-navigation-no-router] **Best for:** single-page apps or dashboards that use tab components instead of the Angular router. Map your active tab to a virtual path and pass it as `currentPath`. The widget appears whenever `currentPath` matches a path in `includeScreens`. ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EmbedProviderComponent } from '@revrag-ai/embed-angular'; interface Tab { id: string; label: string; path: string; } const TABS: Tab[] = [ { id: 'dashboard', label: 'Dashboard', path: '/dashboard' }, { id: 'offers', label: 'Offers', path: '/offers' }, { id: 'settings', label: 'Settings', path: '/settings' }, ]; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, EmbedProviderComponent], template: `
Dashboard content
Offers content β€” widget is visible here
Settings content
`, }) export class AppShellComponent { tabs = TABS; activeTab = 'dashboard'; get currentPath(): string { return this.tabs.find((t) => t.id === this.activeTab)?.path ?? '/'; } setActiveTab(tabId: string): void { this.activeTab = tabId; } } ``` *** ### Pattern 5 β€” With delay and `embedButtonVisibilityConfig` [#pattern-5--with-delay-and-embedbuttonvisibilityconfig] **Best for:** onboarding flows, checkout funnels, or support pages where you want the widget to appear after a delay β€” but only once per session or per group entry. ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs'; import { EmbedProviderComponent, type EmbedButtonVisibilityConfig, } from '@revrag-ai/embed-angular'; @Component({ selector: 'app-shell', standalone: true, imports: [CommonModule, RouterModule, EmbedProviderComponent], template: ` `, }) export class AppShellComponent { currentPath = '/'; allScreens = ['/offers', '/checkout', '/payment', '/support', '/help']; visibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1500, // fallback if a screen isn't in any group groups: [ { // Shows with a 2s delay, every time the user lands here id: 'perScreen', screens: ['/offers'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'perScreen', }, { // Shows with a 2s delay only when entering the checkout flow; // no delay when moving between /checkout and /payment id: 'checkout-flow', screens: ['/checkout', '/payment'], continuity: 'continuous', delayMs: 2000, delayPolicy: 'oncePerGroupEntry', }, { // Shows with a 2s delay only once per browser session id: 'support', screens: ['/support', '/help'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'oncePerAppSession', }, ], }; constructor(private router: Router) { this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((e) => (this.currentPath = e.urlAfterRedirects)); } } ``` *** ### Pattern 6 β€” `startsWith` for nested routes [#pattern-6--startswith-for-nested-routes] **Best for:** feature areas with nested routes where the widget should appear on any sub-page. ```typescript @Component({ template: ` `, }) export class AppShellComponent { // /help, /help/faq, /help/contact β†’ all show the widget // /offers, /offers/123, /offers/details β†’ all show the widget } ``` *** ### Pattern 7 β€” NgModule-based Apps [#pattern-7--ngmodule-based-apps] For applications that have not migrated to standalone components, import `EmbedModule` in your root module: ```typescript // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { EmbedModule } from '@revrag-ai/embed-angular'; import { AppComponent } from './app.component'; import { AppShellComponent } from './app-shell.component'; @NgModule({ declarations: [AppComponent, AppShellComponent], imports: [ BrowserModule, BrowserAnimationsModule, // required EmbedModule, // registers all embed selectors ], bootstrap: [AppComponent], }) export class AppModule {} ``` All component selectors (`revrag-embed-button`, `revrag-embed-provider`) and inputs remain the same. You do **not** need to import individual components when `EmbedModule` is imported. *** ### Pattern 8 β€” With global delay (`embedButtonDelayMs`) [#pattern-8--with-global-delay-embedbuttondelayms] Use the simple `embedButtonDelayMs` input when you want a uniform delay across all included screens without per-group config: ```html ``` The widget will wait 3 seconds after any matching route becomes active before appearing. *** ## Best Practices [#best-practices] 1. **Keep `currentPath` in sync** β€” Always update it from `NavigationEnd.urlAfterRedirects`, not `NavigationStart`, to ensure the URL reflects the fully resolved route. 2. **Use `matchMode: 'startsWith'` for nested routes** β€” Matches `/help` plus any sub-path like `/help/faq`. Use `'exact'` when you need precise control. 3. **Initialize first** β€” Always call `EmbedInitService.initialize()` in `AppComponent.ngOnInit()` and gate widget rendering on `isInitialized$`. 4. **Handle errors** β€” Subscribe to `error$` from `EmbedInitService` and surface it to the user or log it. 5. **Add CSS to `angular.json`** β€” Import `node_modules/@revrag-ai/embed-angular/styles/widget.css` in the `styles` array; rebuild after adding it. 6. **Add `provideAnimations()`** β€” Place it in `app.config.ts` (standalone) or import `BrowserAnimationsModule` (NgModule). Missing it breaks widget transitions silently. 7. **Use `embedButtonVisibilityConfig` for per-route delay policies** β€” Prefer per-group delays over a single global `embedButtonDelayMs` in multi-route apps. 8. **`oncePerAppSession` for support screens** β€” Avoids re-showing the delay on every visit during a session. 9. **`oncePerGroupEntry` for funnels** β€” Delay only when a user first enters a checkout or onboarding flow; no delay when they move between steps. 10. **`continuous` for multi-step flows** β€” Prevents the widget from disappearing and reappearing as the user navigates through `/checkout` β†’ `/payment` β†’ `/confirmation`. 11. **Clean up subscriptions** β€” Unsubscribe from router event subscriptions (or use `takeUntilDestroyed()`) in `ngOnDestroy()` to prevent memory leaks. 12. **Use `embedButtonPosition` for fine-grained placement** β€” When the default `widgetSide` + `widgetBottomOffset` isn't precise enough, pass an `EmbedButtonPosition` object with explicit pixel values. *** ## Summary [#summary] | Pattern | Approach | Best For | | -------------------------- | ---------------------------------------------------- | ------------------------------------ | | **1 β€” Manual** | `*ngIf` + `EmbedButtonComponent` | Explicit imperative control | | **2 β€” Provider + screens** | `EmbedProviderComponent` + `includeScreens` | Router-based apps | | **3 β€” Object config** | Config constant / service β†’ `EmbedProviderComponent` | Shared config, env-driven | | **4 β€” Tab-based** | Virtual paths from active tab state | No-router / dashboard apps | | **5 β€” Visibility config** | `embedButtonVisibilityConfig` with groups | Delay + continuity policies | | **6 β€” startsWith** | `matchMode="startsWith"` | Nested route areas | | **7 β€” NgModule** | `EmbedModule` | Legacy NgModule apps | | **8 β€” Global delay** | `embedButtonDelayMs` | Uniform delay, no group logic needed | Using **`EmbedProviderComponent` + `includeScreens`** is the recommended pattern for most Angular apps: one provider, one config list, the widget shown only on the screens you choose. --- # EmbedWidget advanced (Flutter) > Advanced FAB visibility for Flutter: route allow-lists, multi-flow grouping, pre-show delays and policies, per-flow positioning, and continuity. URL: /embed/integration/embed-flutter-advance Markdown: /embed/integration/embed-flutter-advance.md # EmbedWidget β€” Advanced FAB Visibility (Flutter) [#embedwidget--advanced-fab-visibility-flutter] Advanced control over **when and where** the floating agent button (FAB) appears in your Flutter app: route allow-lists, multi-flow grouping, pre-show delays + policies, per-flow positioning, and continuity. This is the Flutter counterpart of the React Native [EmbedProvider Advanced guide](/embed/integration/embed-provider-advanced). **Terminology:** A React **"group"** maps to a Flutter **"flow"** β€” the string key used in `enabledRoutes`, `groupDelays`, `groupContinuity`, and `groupInsets`. Throughout this doc, *flow = group*. *** ## Overview [#overview] The FAB becomes visible on a screen only when **all** of these are true: | Gate | Controlled by | | --------------------------------------------------- | ------------------------------------ | | Widget enabled | `showEmbedWidget: true` | | Current route is in an enabled flow (or all routes) | `enabledRoutes` / `showOnAllRoutes` | | Route is not blacklisted | `disabledRoutes` | | Agent is live for the flow | backend config (`is_live`) | | Call channel initialized | internal (LiveKit) | | Pre-show delay elapsed | `embedButtonDelayMs` / `groupDelays` | Two integration surfaces: * **`EmbedWidget`** β€” full-control widget that wraps your app. Exposes every visibility option below. * **`EmbedProvider`** β€” convenience wrapper that also calls `embedInitialize` for you. Exposes the common subset (see [Differences](#differences-from-the-react-provider)). *** ## Prerequisites [#prerequisites] | Requirement | Notes | | ------------------------- | ------------------------------------------------------------------------------ | | `embed_flutter` installed | added to `pubspec.yaml` | | SDK initialized | `embedInitialize(apiKey, flowName: ...)` (or use `EmbedProvider`) | | Route detection wired | `EmbedNavigatorObserver`, `EmbedRouteListener`, or manual `setCurrentScreen` | | Route names match | the names you pass to `enabledRoutes` must match the names your router reports | Start here if you have not finished installation, Android/iOS native setup, `embedInitialize`, and a minimal `EmbedWidget` around your `MaterialApp`. *** ## Basic setup [#basic-setup] ### Step 1 β€” Minimal (show on all routes) [#step-1--minimal-show-on-all-routes] ```dart EmbedWidget( showOnAllRoutes: true, child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], home: const HomeScreen(), ), ); ``` ### Step 2 β€” Allow-list specific routes (a flow) [#step-2--allow-list-specific-routes-a-flow] ```dart EmbedWidget( enabledRoutes: const { 'main': ['home_screen', 'profile_plans'], }, child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], home: const HomeScreen(), ), ); ``` ### Step 3 β€” Add a global pre-show delay [#step-3--add-a-global-pre-show-delay] ```dart EmbedWidget( enabledRoutes: const {'main': ['home_screen', 'profile_plans']}, embedButtonDelayMs: 3000, // FAB appears 3s after entering an enabled route child: /* ... */, ); ``` ### Step 4 β€” Full per-flow groups (delay policy, continuity, position) [#step-4--full-per-flow-groups-delay-policy-continuity-position] ```dart EmbedWidget( enabledRoutes: const { 'main': ['home_screen', 'profile_plans'], 'checkout': ['cart_screen', 'payment_screen'], }, embedButtonDelayMs: 2000, // fallback for flows without a groupDelays entry groupDelays: const { 'main': EmbedButtonDelay( delayMs: 4000, policy: EmbedButtonDelayPolicy.perScreen, ), 'checkout': EmbedButtonDelay( delayMs: 1000, policy: EmbedButtonDelayPolicy.oncePerAppSession, ), }, groupContinuity: const { 'checkout': EmbedButtonContinuity.continuous, // stay visible within flow }, groupInsets: const { 'checkout': EmbedButtonInset(right: 16, bottom: 120), }, child: /* ... */, ); ``` *** ## How screen-based visibility works [#how-screen-based-visibility-works] The SDK keeps a single source of truth in `EmbedRouteManager` (a singleton): the **current route** and the **current flow**. On every route change the manager re-evaluates whether the FAB should be active for that route. ``` route change ──▢ EmbedRouteManager.updateRoute(name) β”‚ resolves flow via enabledRoutes + RouteMatchMode β–Ό isEmbedActive? ──▢ EmbedWidget shows / hides the FAB ``` Route names are **normalized** so a leading slash is ignored β€” `/home` and `home` match the same pattern. This makes GoRouter (which reports `/home`) and the standard `Navigator` (which may use `home`) interoperable. *** ## Route detection options [#route-detection-options] Pick whichever fits your routing setup. All three feed the same `EmbedRouteManager`. ### 1. `EmbedNavigatorObserver` (recommended for `Navigator` / GoRouter) [#1-embednavigatorobserver-recommended-for-navigator--gorouter] ```dart MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], // ... ); ``` For GoRouter (which reports full paths) or any custom naming, supply `nameExtractor` to map a route to the name used in `enabledRoutes`: ```dart EmbedNavigatorObserver( nameExtractor: (route) { final path = route.settings.name ?? ''; if (path.startsWith('/home')) return 'home_screen'; // The app's home/initial route is reported as '/' β€” map it explicitly: if (path == '/' ) return 'welcome_screen'; return path.isNotEmpty ? path : null; // return null to ignore a route }, ); ``` ### 2. `EmbedRouteListener` (declarative, per-screen) [#2-embedroutelistener-declarative-per-screen] Wrap a screen's body; it reports `routeName` to the manager on build: ```dart EmbedRouteListener( routeName: 'home_screen', child: HomeScreenBody(), ); ``` ### 3. `EmbedRouteManager().setCurrentScreen(...)` (manual) [#3-embedroutemanagersetcurrentscreen-manual] Framework-agnostic β€” call from a screen's `initState` (useful for shell routes, custom page managers, or the app's initial `home:` route whose name is `/`): ```dart @override void initState() { super.initState(); EmbedRouteManager().setCurrentScreen('welcome_screen'); } ``` *** ## Global delay [#global-delay] `embedButtonDelayMs` keeps the FAB hidden for N milliseconds after the user lands on an enabled route, then shows it with animation. The timer resets when navigating away and back (subject to [delay policy](#visibility-groups-flows)). It is the **fallback** delay: any flow without its own `groupDelays` entry uses this value. Default `0` (appear immediately). The nudge/inactivity countdown starts only **after** the FAB becomes visible (i.e. after this delay), not on route entry. *** ## Visibility groups (flows) [#visibility-groups-flows] A flow groups a set of routes under shared behavior. Define routes with `enabledRoutes`; attach per-flow behavior with the parallel maps `groupDelays`, `groupContinuity`, and `groupInsets` (all keyed by the same flow name). ### `EmbedButtonDelay` [#embedbuttondelay] ```dart EmbedButtonDelay({ required int delayMs, EmbedButtonDelayPolicy policy = EmbedButtonDelayPolicy.perScreen, }); ``` ### `EmbedButtonDelayPolicy` [#embedbuttondelaypolicy] | Value | Behavior | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `perScreen` (default) | Delay runs on **every** screen visit within the flow. With `continuity: keepVisible`, an already-visible FAB stays up (no re-delay). | | `oncePerGroupEntry` | Delay runs only when **entering** the flow; screens visited while staying in the flow skip it. Leaving and re-entering re-applies it. | | `oncePerAppSession` | Delay runs only the **first time** the flow is shown in the app session; later visits show immediately. | Parse from a string with `EmbedButtonDelayPolicy.fromString('oncePerGroupEntry')`. ### `EmbedButtonContinuity` [#embedbuttoncontinuity] | Value | Alias | Behavior | | ------------- | ------------ | ----------------------------------------------------------------------------------------------------- | | `keepVisible` | `continuous` | FAB stays visible when navigating between screens in the same flow β€” no hide/show cycle, no re-delay. | | `reset` | `perScreen` | FAB hides then re-shows on every route change, even within the flow. | Aliases match the React API; `EmbedButtonContinuity.continuous == keepVisible` and `EmbedButtonContinuity.perScreen == reset`. Parse with `EmbedButtonContinuity.fromString('continuous')`. ### Delay-resolution chain [#delay-resolution-chain] ``` groupDelays[flow].delayMs β†’ embedButtonDelayMs β†’ 0 ``` *** ## Insets (positioning) [#insets-positioning] Default position is bottom-right, derived from `rightPadding` / `bottomPadding` (logical pixels). Override per flow with `groupInsets`: ```dart class EmbedButtonInset { final double right; // default 20.0 final double bottom; // default 100.0 const EmbedButtonInset({this.right = 20.0, this.bottom = 100.0}); } ``` ```dart groupInsets: const { 'checkout': EmbedButtonInset(right: 16, bottom: 120), }, ``` A matching flow's inset takes precedence over `rightPadding` / `bottomPadding`. The backend UI config may also carry a `widget_position` (`bottom-left` / `top-right` / …) and padding that influence the corner the FAB anchors to. *** ## Configuration reference [#configuration-reference] ### `EmbedWidget` props [#embedwidget-props] | Prop | Type | Default | Description | | --------------------------- | ------------------------------------- | ------- | ---------------------------------------------------------------- | | `child` | `Widget` | β€” | Your app (typically `MaterialApp`). | | `showEmbedWidget` | `bool` | `true` | Master on/off for the FAB. | | `apiKey` | `String?` | β€” | Optional; usually set via `embedInitialize`. | | `embedUrl` | `String?` | β€” | Backend URL override. | | `enabledRoutes` | `Map>?` | β€” | Flow β†’ routes the FAB is allowed on. | | `showOnAllRoutes` | `bool` | `false` | Show on every route (overrides `enabledRoutes`). | | `disabledRoutes` | `List?` | β€” | Routes where the FAB is always hidden (blacklist). | | `routeMatchMode` | `RouteMatchMode` | `exact` | How routes match patterns: `exact` / `contains` / `startsWith`. | | `embedButtonDelayMs` | `int` | `0` | Global pre-show delay; fallback for flows without `groupDelays`. | | `groupDelays` | `Map?` | β€” | Per-flow delay + `EmbedButtonDelayPolicy`. | | `groupContinuity` | `Map?` | β€” | Per-flow continuity (`keepVisible`/`reset`). | | `groupInsets` | `Map?` | β€” | Per-flow position override. | | `rightPadding` | `double?` | `20` | Default FAB right padding (dp). | | `bottomPadding` | `double?` | `100` | Default FAB bottom padding (dp). | | `onPermissionStatusChanged` | `void Function(bool)?` | β€” | Microphone permission callback. | ### `EmbedProvider` props [#embedprovider-props] Convenience wrapper; also calls `embedInitialize`. Exposes a subset: | Prop | Type | Default | | -------------------------------- | -------------------------------- | ------------ | | `child` | `Widget` | β€” | | `apiKey` | `String` | β€” (required) | | `embedUrl` | `String?` | β€” | | `flowName` | `String?` | β€” | | `enabledRoutes` | `Map>?` | β€” | | `showOnAllRoutes` | `bool` | `false` | | `disabledRoutes` | `List?` | β€” | | `routeMatchMode` | `RouteMatchMode` | `exact` | | `embedButtonDelayMs` | `int` | `0` | | `groupDelays` | `Map?` | β€” | | `rightPadding` / `bottomPadding` | `double?` | `20` / `100` | | `onPermissionStatusChanged` | `void Function(bool)?` | β€” | | `onInitResult` | `void Function(bool, String?)?` | β€” | `EmbedProvider` does **not** currently expose `groupInsets` or `groupContinuity`. Use `EmbedWidget` directly if you need those. ### Types [#types] | Type | Members | | ------------------------ | -------------------------------------------------------------------- | | `RouteMatchMode` | `exact`, `contains`, `startsWith` | | `EmbedButtonDelay` | `delayMs: int`, `policy: EmbedButtonDelayPolicy` | | `EmbedButtonDelayPolicy` | `perScreen`, `oncePerGroupEntry`, `oncePerAppSession` + `fromString` | | `EmbedButtonContinuity` | `keepVisible`(=`continuous`), `reset`(=`perScreen`) + `fromString` | | `EmbedButtonInset` | `right: double = 20`, `bottom: double = 100` | *** ## Usage examples [#usage-examples] ### Example 1 β€” Multi-screen flow + one-shot confirmation flow [#example-1--multi-screen-flow--one-shot-confirmation-flow] ```dart EmbedWidget( enabledRoutes: const { 'shopping': ['catalog', 'product', 'cart'], 'confirm': ['order_confirmed'], }, groupDelays: const { 'shopping': EmbedButtonDelay( delayMs: 3000, policy: EmbedButtonDelayPolicy.oncePerGroupEntry, ), 'confirm': EmbedButtonDelay( delayMs: 0, // immediate on the confirmation screen policy: EmbedButtonDelayPolicy.oncePerAppSession, ), }, groupContinuity: const { 'shopping': EmbedButtonContinuity.continuous, }, child: /* ... */, ); ``` ### Example 2 β€” Two single-screen flows with different delays [#example-2--two-single-screen-flows-with-different-delays] ```dart EmbedWidget( enabledRoutes: const { 'help': ['support'], 'billing': ['invoices'], }, groupDelays: const { 'help': EmbedButtonDelay(delayMs: 1000), 'billing': EmbedButtonDelay(delayMs: 5000), }, child: /* ... */, ); ``` ### Example 3 β€” Global delay only (no per-flow config) [#example-3--global-delay-only-no-per-flow-config] ```dart EmbedWidget( enabledRoutes: const {'main': ['home', 'profile']}, embedButtonDelayMs: 2000, // applies to every enabled route child: /* ... */, ); ``` *** ## Practical scenarios [#practical-scenarios] | Scenario | Configuration | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | **Let users read a form before the FAB appears** | `embedButtonDelayMs: 4000` (or a per-flow `EmbedButtonDelay`). | | **Keep the FAB steady across a multi-step flow** | `groupContinuity: {'flow': EmbedButtonContinuity.continuous}` + `oncePerGroupEntry` delay. | | **Show the FAB only once per session, then instantly** | `EmbedButtonDelay(delayMs: 2000, policy: oncePerAppSession)`. | | **Avoid overlapping a bottom bar on one flow** | `groupInsets: {'flow': EmbedButtonInset(bottom: 140)}`. | | **Hide the FAB on auth/splash screens** | omit them from `enabledRoutes`, or add to `disabledRoutes`. | *** ## Differences from the React provider [#differences-from-the-react-provider] | React Native | Flutter | Notes | | ----------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------- | | `includeScreens` | `enabledRoutes` | Flutter groups routes under a flow key. | | `embedButtonVisibilityConfig` (single object) | `groupDelays` + `groupContinuity` + `groupInsets` (parallel maps) | Same capabilities, different shape. | | group `delayMs` / `delayPolicy` | `EmbedButtonDelay` | βœ… | | group `continuity` (`continuous`/`perScreen`) | `EmbedButtonContinuity` (+ aliases) | βœ… | | `embedButtonDelayMs` | `embedButtonDelayMs` | βœ… | | `defaultDelayMs` | β€” | covered by global `embedButtonDelayMs`. | | `EmbedButtonInset { top, right, bottom, left }` | `EmbedButtonInset { right, bottom }` | ⚠️ Flutter supports right/bottom only. | | `defaultInset` | β€” | use `rightPadding`/`bottomPadding`. | | `navigationRef` | `EmbedNavigatorObserver` / `EmbedRouteListener` / `setCurrentScreen` | route detection. | | `appVersion` | β€” | not exposed. | *** ## Troubleshooting [#troubleshooting] | Symptom | Likely cause | Fix | | ------------------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | FAB never appears on a route | route name doesn't match `enabledRoutes` | confirm the name your router reports; use `nameExtractor` or `setCurrentScreen`. | | FAB missing on the **initial/home** screen | `home:` route is reported as `/`, not your name | map `/` in `nameExtractor`, or call `setCurrentScreen('...')` in that screen's `initState`. | | FAB appears off-screen | stale/zero screen size at first layout | ensure you're on a recent SDK (position is recomputed post-layout). | | Delay never elapses / FAB stays hidden | very large `delayMs`, or route flips inactive before it fires | verify the route stays in the flow; check `embedButtonDelayMs`/`groupDelays`. | | FAB flickers between screens in a flow | continuity is `reset` | set `EmbedButtonContinuity.keepVisible` (`continuous`) for that flow. | *** ## Best practices [#best-practices] 1. **Match route names exactly** to what your router reports (after slash normalization). Prefer one detection mechanism (observer *or* listener *or* manual) per app. 2. **Use flows as groups** β€” one flow per logical journey (checkout, onboarding). 3. **Prefer `oncePerGroupEntry` / `oncePerAppSession`** for delays inside multi-screen flows so users aren't re-delayed on every screen. 4. **Pair `continuous` continuity with `oncePerGroupEntry`** for a steady FAB across a flow. 5. **Keep delays short** (1–4s) β€” long delays read as "no button". 6. **Use `groupInsets`** to dodge bottom bars/FABs on specific flows rather than moving the global padding. *** ## Related documentation [#related-documentation] Installation, native LiveKit setup, `embedInitialize`, navigator integration, events, and troubleshooting. The React Native counterpart: `includeScreens`, `embedButtonDelayMs`, and `embedButtonVisibilityConfig`. *** *For additional help visit [https://docs.revrag.ai/embed/integration/flutter](https://docs.revrag.ai/embed/integration/flutter) or email [contact@revrag.ai](mailto:contact@revrag.ai).* --- # EmbedProvider advanced > Advanced EmbedProvider patterns for React Native: screen visibility, route groups, delays, continuity, and FAB insets. URL: /embed/integration/embed-provider-advanced Markdown: /embed/integration/embed-provider-advanced.md # EmbedProvider advanced (FAB visibility) [#embedprovider-advanced-fab-visibility] Use this guide after you have a working **`EmbedProvider`** + **`NavigationContainer`** setup from the [React Native integration guide](/embed/integration/react-native). Here you tune **where** the FAB appears, **when** it shows, and **how** it is positioned using **`includeScreens`**, **`embedButtonDelayMs`**, and **`embedButtonVisibilityConfig`**. *** ## Introduction [#introduction] **EmbedProvider** wraps your app and: * Listens to **React Navigation** state and shows or hides **`EmbedButton`** by screen. * Supports **visibility groups**: per-group delay, continuity, and inset. * Enriches analytics with **screen context** (current screen, path, depth). **What this guide covers** * **`includeScreens`** - allowlist route names where the FAB may appear. * **`embedButtonDelayMs`** - default delay before the FAB appears on an eligible screen. * **`embedButtonVisibilityConfig`** - **groups**, **continuity**, **delay policies**, and **insets** for production-grade UX. **Requirements** * **React Navigation** (for example `@react-navigation/native`). * **`EmbedProvider` must wrap `NavigationContainer`** and use the **same `ref`** you pass to **`NavigationContainer`**. *** ## Prerequisites [#prerequisites] Before you use advanced visibility rules, confirm the following: | Requirement | Notes | | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **React Native** | 0.70 or higher (same as main integration guide) | | **React Navigation** | Root **`NavigationContainer`** with a shared **`navigationRef`** | | **Base embed setup** | **`useInitialize`**, **`GestureHandlerRootView`**, and **`EmbedProvider`** wired as in the main guide | **You will also need:** * Route **`name`** values that match your navigators exactly (**case-sensitive**). * A clear idea of which flows should show the FAB (tabs, stacks, auth exclusions, etc.). If **`navigationRef`** is missing or not the same ref as on **`NavigationContainer`**, the provider cannot detect screen changes and FAB visibility will not match your rules. *** ## Before you continue: base integration [#before-you-continue-base-integration] Complete **installation**, **native LiveKit setup**, and the **Basic setup** steps in the React Native guide first. Advanced props build on that tree. Start here if you have not finished **`useInitialize`**, peer dependencies, **`GestureHandlerRootView`**, and a minimal **`EmbedProvider`** around **`NavigationContainer`**. *** ## Basic setup (step-by-step) [#basic-setup-step-by-step] Follow these in order. You can stop after Step 2 for a simple allowlist-only integration. ### Step 1 - Minimal provider and ref [#step-1---minimal-provider-and-ref] ```tsx import { useRef } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { EmbedProvider } from '@revrag-ai/embed-react-native'; import { RootNavigator } from './navigation/RootNavigator'; export default function App() { const navigationRef = useRef(null); return ( ); } ``` * **`appVersion`** (required): Your app version string (for example from **`package.json`**). Used in analytics. * **`navigationRef`**: Must be the ref attached to **`NavigationContainer`**. Without it, the provider cannot reliably drive screen-based visibility. ### Step 2 - Restrict screens with includeScreens (optional) [#step-2---restrict-screens-with-includescreens-optional] ```tsx ``` * **`includeScreens`**: Route **names** where the button may appear. If omitted or empty, eligibility depends on backend and groups (see visibility groups). Names must match **`Stack.Screen name="..."`** exactly. ### Step 3 - Global delay (optional) [#step-3---global-delay-optional] Delay the first appearance of the FAB after entering an included screen: ```tsx ``` * **`embedButtonDelayMs`**: Delay in **milliseconds** before showing the FAB on an included screen. Default **`0`**. Groups can override per group. ### Step 4 - Visibility groups (optional, advanced) [#step-4---visibility-groups-optional-advanced] For different delays, continuity, or insets per flow, configure **`embedButtonVisibilityConfig`** (see [Visibility groups](#visibility-groups) and [Configuration options](#configuration-options)). *** ## How screen-based visibility works [#how-screen-based-visibility-works] 1. You pass a **ref** from your root to both **`EmbedProvider`** and **`NavigationContainer`**. 2. The provider subscribes to navigation **state** and reads the current route (deepest active screen). 3. If the current screen is in **`includeScreens`** or in any **visibility group** when using groups, the FAB is shown; otherwise it is hidden. **Important** * **`EmbedProvider` must wrap `NavigationContainer`** so the listener uses the same ref. * Route names are **case-sensitive** and must match exactly (for example **`Screen1`**, not **`screen1`**). *** ## Global delay [#global-delay] See **Step 3** above. **`embedButtonDelayMs`** is the global default; **`embedButtonVisibilityConfig.defaultDelayMs`** and per-group **`delayMs`** override or refine behavior when you use groups. *** ## Visibility groups [#visibility-groups] For finer control (different delays, staying visible across screens, per-group position), use **visibility groups**. ### Concepts [#concepts] | Concept | Meaning | | ---------------- | --------------------------------------------------------------------------------- | | **Group** | A set of screens that share delay, continuity, and inset rules. | | **Continuity** | Whether the FAB stays visible when moving between screens in the same group. | | **Delay policy** | When the delay runs: every screen, once per group entry, or once per app session. | | **Inset** | Distance from screen edges (right, bottom, and so on) for the FAB. | ### Types (import from the package) [#types-import-from-the-package] ```ts import type { EmbedButtonVisibilityConfig, EmbedButtonGroupConfig, EmbedButtonContinuity, EmbedButtonDelayPolicy, EmbedButtonInset, } from '@revrag-ai/embed-react-native'; ``` ### `EmbedButtonVisibilityConfig` [#embedbuttonvisibilityconfig] ```ts interface EmbedButtonVisibilityConfig { defaultDelayMs?: number; // Optional fallback when a group does not set delayMs defaultInset?: EmbedButtonInset; groups?: EmbedButtonGroupConfig[]; } ``` **When do you need `defaultDelayMs`?** You do not need it if every group sets its own **`delayMs`**. It is a **fallback** when: * A **group omits `delayMs`** - that group falls back to **`defaultDelayMs`**, then top-level **`embedButtonDelayMs`**. * A screen is **included** (for example via **`includeScreens`**) but **does not belong to any group** - the provider uses **`defaultDelayMs`** (or **`embedButtonDelayMs`**) for that screen. You can omit **`defaultDelayMs`** and **`defaultInset`** when every group defines its own **`delayMs`** and **`inset`**. ### `EmbedButtonGroupConfig` [#embedbuttongroupconfig] ```ts interface EmbedButtonGroupConfig { id: string; // Unique ID for this group screens: string[]; // Route names in this group continuity: EmbedButtonContinuity; inset?: EmbedButtonInset; delayMs?: number; delayPolicy?: EmbedButtonDelayPolicy; } ``` ### `EmbedButtonContinuity` [#embedbuttoncontinuity] * **`continuous`** - Moving between screens **in the same group** keeps the FAB visible; delay is **not** re-applied in the way **`perScreen`** would. * **`perScreen`** - Each screen in the group is treated independently (delay can re-run per screen if the policy allows). ### `EmbedButtonDelayPolicy` [#embedbuttondelaypolicy] * **`perScreen`** - Delay runs on **every** included screen in the group when you land on it. * **`oncePerGroupEntry`** - Delay runs when **entering** the group (first screen of that visit). Moving within the group does not re-trigger the delay (pairs well with **`continuous`**). * **`oncePerAppSession`** - Delay runs **once per app session** for that group; later visits to the group show the FAB immediately per policy. ### Including screens via groups [#including-screens-via-groups] Screens listed in **any** group **`screens`** array count as **included** even if you omit them from **`includeScreens`**. You can: * Use **only groups** (for example omit the allowlist and define all included screens inside **groups**), or * Use **both** - the final included set is the **union** of **`includeScreens`** and all group screens. *** ## Insets (button position) [#insets-button-position] **`EmbedButtonInset`** controls distance from screen edges: ```ts type EmbedButtonInset = { top?: number | string; right?: number | string; bottom?: number | string; left?: number | string; }; ``` * Values are usually **numbers** (for example **`16`**, **`54`**). * Set **per group** in **`EmbedButtonGroupConfig.inset`**, or a default in **`EmbedButtonVisibilityConfig.defaultInset`**. * If unset, the SDK uses internal defaults (for example **right: 16**, **bottom: 20**). ```ts const flowGroup: EmbedButtonGroupConfig = { id: 'mainFlow', screens: ['Screen1', 'Screen2', 'Screen3'], continuity: 'continuous', inset: { right: 16, bottom: 54 }, delayMs: 1500, delayPolicy: 'oncePerGroupEntry', }; ``` *** ## Configuration options [#configuration-options] ### `EmbedProvider` props [#embedprovider-props] | Prop | Type | Required | Description | | --------------------------------- | ----------------------------- | ----------- | ------------------------------------------------------------------------------- | | **`children`** | `ReactNode` | Yes | Your app, usually **`NavigationContainer`** and below. | | **`navigationRef`** | ref | Recommended | Same ref as **`NavigationContainer`** so route changes are observed. | | **`appVersion`** | `string` | Yes | App version for analytics. | | **`includeScreens`** | `string[]` | No | Route names where the FAB may appear; union with group screens if both are set. | | **`embedButtonDelayMs`** | `number` | No | Default delay (ms) before showing the FAB when no group overrides apply. | | **`embedButtonVisibilityConfig`** | `EmbedButtonVisibilityConfig` | No | Groups, continuity, per-group delays, insets. | ### `EmbedButtonVisibilityConfig` [#embedbuttonvisibilityconfig-1] | Field | Description | | -------------------- | -------------------------------------------------------------- | | **`defaultDelayMs`** | Used when a matched group does not specify **`delayMs`**. | | **`defaultInset`** | Default inset when a group does not specify **`inset`**. | | **`groups`** | Array of **`EmbedButtonGroupConfig`**. | ### `EmbedButtonGroupConfig` [#embedbuttongroupconfig-1] | Field | Description | | ----------------- | ----------------------------------------------------------------------------------------- | | **`id`** | Unique string for the group. | | **`screens`** | Screen **names** in this group. | | **`continuity`** | **`continuous`** or **`perScreen`**. | | **`delayMs`** | Delay in ms for this group. | | **`delayPolicy`** | **`perScreen`**, **`oncePerGroupEntry`**, or **`oncePerAppSession`**. | | **`inset`** | Offsets from edges **`{ top, right, bottom, left }`**. | *** ## Usage examples [#usage-examples] ### Example 1: Multi-screen flow and confirmation screen [#example-1-multi-screen-flow-and-confirmation-screen] * **Main flow**: Screen1 to Screen3. FAB after 1.5s when entering the flow, **stays visible** within the flow. Inset **right: 16**, **bottom: 54**. * **Screen4**: Single screen. Delay 1.5s **once per app session**. Inset **right: 24**, **bottom: 32**. * **Other screens**: No FAB. ```tsx import { EmbedProvider, type EmbedButtonContinuity, type EmbedButtonDelayPolicy, type EmbedButtonGroupConfig, type EmbedButtonVisibilityConfig, } from '@revrag-ai/embed-react-native'; import { NavigationContainer } from '@react-navigation/native'; import { useRef } from 'react'; const flowGroup: EmbedButtonGroupConfig = { id: 'mainFlow', screens: ['Screen1', 'Screen2', 'Screen3'], continuity: 'continuous' as EmbedButtonContinuity, inset: { right: 16, bottom: 54 }, delayMs: 1500, delayPolicy: 'oncePerGroupEntry' as EmbedButtonDelayPolicy, }; const confirmationGroup: EmbedButtonGroupConfig = { id: 'confirmationScreen', screens: ['Screen4'], continuity: 'perScreen' as EmbedButtonContinuity, inset: { right: 24, bottom: 32 }, delayMs: 1500, delayPolicy: 'oncePerAppSession' as EmbedButtonDelayPolicy, }; const embedButtonVisibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1200, defaultInset: { right: 16, bottom: 20 }, groups: [flowGroup, confirmationGroup], }; export default function App() { const navigationRef = useRef(null); return ( ); } ``` ### Example 2: Two screens with different delays [#example-2-two-screens-with-different-delays] * **ScreenA**: Delay 1.5s every visit, inset **right: 16**, **bottom: 24**. * **ScreenB**: Delay 3s every visit, same inset. ```tsx const screenAGroup: EmbedButtonGroupConfig = { id: 'groupA', screens: ['ScreenA'], continuity: 'perScreen', inset: { right: 16, bottom: 24 }, delayMs: 1500, delayPolicy: 'perScreen', }; const screenBGroup: EmbedButtonGroupConfig = { id: 'groupB', screens: ['ScreenB'], continuity: 'perScreen', inset: { right: 16, bottom: 24 }, delayMs: 3000, delayPolicy: 'perScreen', }; const embedButtonVisibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1200, defaultInset: { right: 16, bottom: 20 }, groups: [screenAGroup, screenBGroup], }; ``` ### Example 3: Screens included only via groups [#example-3-screens-included-only-via-groups] Omit **`includeScreens`**; only group membership decides visibility: ```tsx ``` Screens that appear in at least one group **`screens`** array get the FAB; all others do not. *** ## Practical scenarios [#practical-scenarios] ### Multi-step form flow (single delay) [#multi-step-form-flow-single-delay] Goal: Show delay once, then keep the FAB visible across steps. ```tsx const flowGroup: EmbedButtonGroupConfig = { id: 'formFlow', screens: ['Step1', 'Step2', 'Step3', 'Step4', 'Step5'], continuity: 'continuous' as EmbedButtonContinuity, delayMs: 1500, delayPolicy: 'oncePerGroupEntry' as EmbedButtonDelayPolicy, }; ``` ### Same flow with extra standalone screens [#same-flow-with-extra-standalone-screens] Goal: Delay once for the flow, different behavior for other screens. ```tsx const flowGroup: EmbedButtonGroupConfig = { id: 'formFlow', screens: ['Step1', 'Step2', 'Step3', 'Step4', 'Step5'], continuity: 'continuous' as EmbedButtonContinuity, delayMs: 1500, delayPolicy: 'oncePerGroupEntry' as EmbedButtonDelayPolicy, }; const otherGroup: EmbedButtonGroupConfig = { id: 'otherScreens', screens: ['ScreenX', 'ScreenY'], continuity: 'perScreen' as EmbedButtonContinuity, delayMs: 1200, delayPolicy: 'perScreen' as EmbedButtonDelayPolicy, }; ``` ### Avoid overlapping bottom UI [#avoid-overlapping-bottom-ui] Goal: Push the FAB above a bottom tab bar. ```tsx const flowGroup: EmbedButtonGroupConfig = { id: 'formFlow', screens: ['Step1', 'Step2', 'Step3'], continuity: 'continuous' as EmbedButtonContinuity, inset: { right: 16, bottom: 64 }, delayMs: 1200, delayPolicy: 'oncePerGroupEntry' as EmbedButtonDelayPolicy, }; ``` *** ## Troubleshooting [#troubleshooting] | Issue | What to check | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FAB never appears | 1) **`navigationRef`** matches **`NavigationContainer`**. 2) **`EmbedProvider`** wraps **`NavigationContainer`**. 3) Current route is in **`includeScreens`** or a group **`screens`** list. | | FAB on wrong screens | Route **`name`** values must match **exactly** (case-sensitive) your **`Stack.Screen`** (or equivalent) **`name`**. | | Screen not interactive when FAB is visible | Overlay uses **`pointerEvents="box-none"`** so touches pass through except on the FAB. Update the SDK if behavior differs. | | Wrong position | Set **`inset`** per group or **`defaultInset`**. Insets apply inside the FAB container on the full-screen overlay. | | Delay feels wrong | Check **`delayPolicy`** and **`continuity`**: **`oncePerGroupEntry`** + **`continuous`** delays once on group entry; **`perScreen`** can re-apply on each screen. | | Types not found | Import from **`@revrag-ai/embed-react-native`**: **`EmbedButtonVisibilityConfig`**, **`EmbedButtonGroupConfig`**, **`EmbedButtonContinuity`**, **`EmbedButtonDelayPolicy`**, **`EmbedButtonInset`**. | 1. Log the active route name and compare to **`includeScreens`** / group **`screens`**. 2. Confirm **`EmbedProvider`** is **outside** **`NavigationContainer`** and **`ref`** is the same object reference. 3. Temporarily set a broad **`includeScreens`** list to verify routing, then tighten. *** ## Best practices [#best-practices] * **Match route names in config to navigator `name` props** - typos and casing break visibility. * **Start with `includeScreens`**, then add **groups** when you need different delays or insets per flow. * **Prefer `oncePerGroupEntry` + `continuous`** for multi-step flows so users do not see the FAB pop in on every step. * **Tune `bottom` inset** when you have tab bars or bottom sheets so the FAB does not overlap primary UI. * **Keep `appVersion`** accurate for analytics when debugging screen context. * **Re-read delay policies** when QA reports "delay only happened once" - often **`oncePerAppSession`** or **`oncePerGroupEntry`** is working as designed. *** ## Support [#support] * **Docs:** [https://docs.revrag.ai](https://docs.revrag.ai/) * **Email:** [contact@revrag.ai](mailto:contact@revrag.ai) **Main integration:** [React Native integration](/embed/integration/react-native) (install, native setup, **`useInitialize`**, **`USER_DATA`**, events). *** ## Related documentation [#related-documentation] Installation, native LiveKit setup, **`GestureHandlerRootView`**, **`useInitialize`**, **`EmbedProvider`** basics, **`Embed.Event`**, and troubleshooting. --- # EmbedProvider advanced (React) > Advanced EmbedProvider patterns for React: route visibility, delay policies, programmatic control, and production best practices. URL: /embed/integration/embed-react-provider-advanced Markdown: /embed/integration/embed-react-provider-advanced.md # EmbedProvider β€” Basic to Advanced Guide for React [#embedprovider--basic-to-advanced-guide-for-react] `EmbedProvider` is a React context provider that manages **where** and **when** the AI widget appears across your app. Instead of manually placing `` on every page, you wrap your app once and let the provider handle everything. *** ## How It Works (Internals Overview) [#how-it-works-internals-overview] ``` EmbedProvider β”œβ”€β”€ Detects current route (via usePathHook β†’ currentPath prop β†’ window.location fallback) β”œβ”€β”€ Checks if the route is in includeScreens β”œβ”€β”€ Applies delay logic (embedButtonDelayMs / group config) └── Renders when conditions are met ``` * If `includeScreens` is **empty** β†’ widget shows on **every route** * If `includeScreens` has values β†’ widget shows **only on those routes** * Path detection priority: `currentPath` prop > `usePathHook` > `window.location` *** ## Level 1 β€” Show Widget on Every Page [#level-1--show-widget-on-every-page] The simplest setup. No route filtering, no delays. ```tsx // app/layout.tsx (Next.js) or your root component "use client"; import { useInitialize, EmbedProvider } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/dist/ai-assistant-widget.css'; export default function RootLayout({ children }) { useInitialize("your-api-key"); return ( {children} ); } ``` The widget appears on every route immediately. That's it. *** ## Level 2 β€” Show Widget Only on Specific Routes [#level-2--show-widget-only-on-specific-routes] Use `includeScreens` to whitelist which routes show the widget. ### Exact Match (default) [#exact-match-default] `/help` matches **only** `/help` β€” not `/help/faq`. ```tsx {children} ``` ### Prefix Match [#prefix-match] `/help` matches `/help`, `/help/faq`, `/help/contact`, etc. ```tsx {children} ``` ### Injecting the Router Hook [#injecting-the-router-hook] The provider needs to know the current path. Pass your router's hook so it updates on navigation. **Next.js App Router:** ```tsx import { usePathname } from 'next/navigation'; // Wrap in a named function β€” required because hooks must be // called unconditionally and stably inside the provider function useNextPathname() { return usePathname(); } ``` **React Router:** ```tsx import { useLocation } from 'react-router-dom'; function useReactRouterPath() { return useLocation().pathname; } ``` **Manual override (any framework):** ```tsx // Pass the path directly β€” highest priority, overrides everything ``` *** ## Level 3 β€” Delay the Widget Appearance [#level-3--delay-the-widget-appearance] Show the widget after the user has been on a screen for a few seconds, so it doesn't feel intrusive. ```tsx {children} ``` **What happens:** 1. User navigates to `/pricing` 2. Provider hides the widget and starts a 4-second timer 3. Timer fires β†’ widget appears with animation 4. User navigates away β†’ widget hides immediately, timer resets 5. User comes back to `/pricing` β†’ 4-second timer starts again *** ## Level 4 β€” Customize the Button Position [#level-4--customize-the-button-position] Override where the floating button sits on the screen. ```tsx {children} ``` You can also pass CSS strings: ```tsx embedButtonPosition={{ bottom: '5rem', right: '1.5rem' }} ``` Pass any `EmbedButton` prop through `embedButtonProps`: ```tsx {children} ``` *** ## Level 5 β€” Group-Based Visibility [#level-5--group-based-visibility] This is the advanced visibility engine. Use it when different sections of your app need different delay or continuity behavior. ### The Problem It Solves [#the-problem-it-solves] Without groups, every navigation triggers the delay timer β€” so if a user moves between `/checkout` and `/payment` (both part of checkout), the widget keeps hiding and re-appearing. Groups prevent that. ### Core Concepts [#core-concepts] **`continuity`** β€” controls what happens when navigating *within* a group: | Value | Behavior | | -------------- | ------------------------------------------------------------------------------------ | | `"continuous"` | Widget stays visible β€” no re-animation when moving between screens in the same group | | `"perScreen"` | Widget re-applies delay on every screen, even within the group | **`delayPolicy`** β€” controls *when* the delay fires: | Value | Behavior | | --------------------- | ----------------------------------------------------------- | | `"perScreen"` | Delay fires on every screen in the group | | `"oncePerGroupEntry"` | Delay fires only the first time the user enters this group | | `"oncePerAppSession"` | Delay fires at most once per browser session for this group | ### Basic Groups Example [#basic-groups-example] ```tsx import { EmbedProvider } from '@revrag-ai/embed-react'; import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-react'; const visibilityConfig: EmbedButtonVisibilityConfig = { groups: [ { id: 'checkout-flow', screens: ['/cart', '/checkout', '/payment', '/confirmation'], continuity: 'continuous', // no re-animation between checkout steps delayMs: 2000, delayPolicy: 'oncePerGroupEntry', // delay only on first entry to checkout }, ], }; {children} ``` **What happens:** 1. User is on `/home` β†’ widget hidden (not in any group) 2. User goes to `/cart` β†’ 2-second delay, then widget appears 3. User goes to `/checkout` β†’ widget stays visible (same group, `continuous`) 4. User goes to `/payment` β†’ widget stays visible (same group, `continuous`) 5. User leaves to `/home` β†’ widget hidden 6. User comes back to `/cart` β†’ **no delay** this time (`oncePerGroupEntry` β€” already triggered) ### Multiple Groups [#multiple-groups] ```tsx const visibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1000, // fallback delay for screens not in any group groups: [ { id: 'onboarding', screens: ['/welcome', '/setup', '/profile-setup'], continuity: 'continuous', delayMs: 5000, delayPolicy: 'oncePerAppSession', // only delays once per browser session }, { id: 'checkout', screens: ['/cart', '/checkout', '/payment'], continuity: 'continuous', delayMs: 2000, delayPolicy: 'oncePerGroupEntry', }, { id: 'support', screens: ['/help', '/faq', '/contact'], continuity: 'perScreen', // re-animate on every support page delayMs: 3000, delayPolicy: 'perScreen', }, ], }; ``` ### defaultDelayMs [#defaultdelayms] Applies to any screen that is **included** (via `includeScreens`) but **not in any group**: ```tsx {children} ``` *** ## Level 6 β€” Reading Current Path in Children [#level-6--reading-current-path-in-children] Any component inside `EmbedProvider` can access the current path via `useEmbed`: ```tsx import { useEmbed } from '@revrag-ai/embed-react'; function Breadcrumb() { const { currentPath } = useEmbed(); return ; } ``` > `useEmbed()` throws if called outside `EmbedProvider`. Always use it inside the provider tree. *** ## Complete Real-World Example [#complete-real-world-example] A Next.js app with multiple sections, each with their own widget behavior: ```tsx // app/layout.tsx "use client"; import { usePathname } from 'next/navigation'; import { useInitialize, EmbedProvider } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/dist/ai-assistant-widget.css'; import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-react'; function useNextPathname() { return usePathname(); } const visibilityConfig: EmbedButtonVisibilityConfig = { defaultDelayMs: 1500, groups: [ { // Onboarding: delay once per session, stay visible through all steps id: 'onboarding', screens: ['/welcome', '/setup', '/verify'], continuity: 'continuous', delayMs: 6000, delayPolicy: 'oncePerAppSession', }, { // Checkout: delay once per entry, no re-animation between steps id: 'checkout', screens: ['/cart', '/checkout', '/payment', '/order-confirmed'], continuity: 'continuous', delayMs: 3000, delayPolicy: 'oncePerGroupEntry', }, { // Support: always delay, re-animate on each page (high intent section) id: 'support', screens: ['/help', '/faq', '/contact'], continuity: 'perScreen', delayMs: 2000, delayPolicy: 'perScreen', }, ], }; export default function RootLayout({ children }: { children: React.ReactNode }) { const { error } = useInitialize("your-api-key"); if (error) console.error('[EmbedSDK] Init error:', error); return ( {children} ); } ``` *** ## Props Quick Reference [#props-quick-reference] | Prop | Type | Default | Purpose | | ----------------------------- | ----------------------------- | --------------------------- | ---------------------------------------- | | `children` | `ReactNode` | β€” | Your app content | | `currentPath` | `string` | β€” | Manual path override (highest priority) | | `usePathHook` | `() => string` | β€” | Router hook for automatic path detection | | `includeScreens` | `string[]` | `[]` (all) | Routes where the widget appears | | `matchMode` | `"exact" \| "startsWith"` | `"exact"` | How routes are matched | | `embedButtonDelayMs` | `number` | `0` | Global delay before widget appears (ms) | | `embedButtonVisibilityConfig` | `EmbedButtonVisibilityConfig` | β€” | Advanced group-based visibility | | `embedButtonProps` | `EmbedButtonProps` | β€” | Props forwarded to `` | | `embedButtonPosition` | `{ bottom?, right? }` | `{ bottom: 20, right: 16 }` | Fixed position of the floating button | *** ## Common Mistakes [#common-mistakes] **Passing the hook result instead of the hook itself:** ```tsx // ❌ Wrong β€” passes the path string, not the hook // βœ… Correct β€” passes the hook function function useNextPathname() { return usePathname(); } ``` **Using `useEmbed` outside the provider:** ```tsx // ❌ Throws an error function ComponentOutsideProvider() { const { currentPath } = useEmbed(); // Error! } // βœ… Must be inside EmbedProvider tree function ComponentInsideProvider() { const { currentPath } = useEmbed(); // Works } ``` **Expecting `includeScreens` + groups to be separate:** Screens listed in `groups[].screens` are **automatically added** to the include list β€” you don't need to repeat them in `includeScreens`. ```tsx // βœ… You don't need to list /help in includeScreens β€” it's already in the group ``` **Forgetting `"use client"` in Next.js App Router:** ```tsx // βœ… Required when using EmbedProvider in Next.js App Router "use client"; import { EmbedProvider } from '@revrag-ai/embed-react'; ``` --- # Flutter > Step-by-step guide to integrate the RevRag Flutter embed SDK (voice agent, EmbedWidget, navigation-aware visibility, events, configuration, and native platform setup). URL: /embed/integration/flutter Markdown: /embed/integration/flutter.md # Embed Flutter SDK [#embed-flutter-sdk] Follow this guide in order the first time you integrate. **Native microphone setup is required** on both Android and iOS β€” skipping it is the most common source of "the mic dialog never appears" and silent voice failures. *** ## Introduction [#introduction] The **`embed_flutter`** SDK adds a **voice AI agent** to your app: a **floating action button (FAB)** backed by **LiveKit**, **navigation-aware** visibility, and a **user-context channel** to your embed backend. **Package:** `embed_flutter` **Β·** **Version:** `0.1.0` **Β·** **Requires:** Flutter β‰₯ 3.0.0 / Dart β‰₯ 3.0.0 **What you get out of the box** * **Realtime voice** with the agent through the FAB * **Screen and app context** for richer conversations (route tracking via `EmbedNavigatorObserver` / `EmbedRouteListener`, optional explicit `SCREEN_STATE`) * **Event tracking**: host-driven analytics and custom payloads via `embedEvent`, plus **agent lifecycle** signals (`embedOnAgent`) * **Server-driven UI** for the FAB via `widget_config` from device registration * **Advanced FAB behavior** (route **flows/groups**, show **delays** + policies, **insets**, continuity rules): covered in **[EmbedWidget advanced](/embed/integration/embed-flutter-advance)** β€” read it once you move past a simple `enabledRoutes` list *** ## 1. Installation [#1-installation] Add the package to your `pubspec.yaml`: ```yaml dependencies: embed_flutter: ^0.1.0 ``` Then fetch: ```bash flutter pub get ``` ### 1.1 Android Setup [#11-android-setup] Open `android/app/src/main/AndroidManifest.xml` and add: ```xml ``` ### 1.2 iOS Setup [#12-ios-setup] **Step 1 β€” Info.plist** Add the microphone usage description to your `ios/Runner/Info.plist`: ```xml NSMicrophoneUsageDescription This app needs microphone access for voice calls. ``` **Step 2 β€” Podfile** The SDK requires explicit configuration in your `ios/Podfile` to enable microphone permission requests. Add the `PERMISSION_MICROPHONE=1` macro inside your `post_install` block: ```ruby post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.microphone 'PERMISSION_MICROPHONE=1', # ...other permissions... ] end end end ``` Without the `PERMISSION_MICROPHONE=1` macro in the Podfile, the microphone permission dialog will never appear on iOS and the permission will be reported as permanently denied β€” even if it has not been requested before. After editing the Podfile, run: ```bash cd ios && pod install && cd .. ``` *** ## 2. How the SDK Works (Mental Model) [#2-how-the-sdk-works-mental-model] Understanding these three concepts up front makes integration straightforward: | Concept | Description | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **EmbedWidget** | A full-screen overlay wrapper placed at the very top of your widget tree. It renders the floating AI agent button and manages the live voice session. | | **EmbedNavigatorObserver / EmbedRouteListener** | Tells the SDK which screen the user is on so it knows whether to show the agent button. | | **enabledRoutes** | A map of route names where the agent button should appear. Only routes listed here will show the agent. | **Order of operations every time:** ``` main() └─ embedInitialize(apiKey, flowName: 'main') ← before runApp └─ runApp( EmbedWidget( ← outermost widget enabledRoutes: { 'main': ['home', 'product', 'cart'], }, child: MaterialApp / Router( navigatorObservers: [EmbedNavigatorObserver()], ← required ... ), ), ) After authentication β†’ embedEvent(USER_DATA, ...) ← activates the agent ``` *** ## 3. Initialization [#3-initialization] Call `embedInitialize()` **before** `runApp()`. ```dart import 'package:flutter/material.dart'; import 'package:embed_flutter/embed_flutter.dart'; void main() { embedInitialize( 'your-api-key', flowName: 'main', // identifies your agent configuration embedUrl: 'https://embed.revrag.ai', // optional; defaults to revrag.ai onResult: (bool success, String? error) { // Called after the SDK prefetches the UI config from the server. if (!success) { print('Embed init failed: $error'); } }, ); runApp(const MyApp()); } ``` **Optional: set app version** (sent with every analytics event): ```dart embedSetAppVersion('2.4.1'); ``` *** ## 4. Wrapping Your App with EmbedWidget [#4-wrapping-your-app-with-embedwidget] `EmbedWidget` must be the **outermost** widget β€” it wraps your `MaterialApp` / `MaterialApp.router` / `CupertinoApp`. ```dart class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, // List the routes where the agent button should appear enabledRoutes: const { 'main': ['home', 'product_list', 'product_detail', 'cart'], }, child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], // ... rest of your app ), ); } } ``` The agent button appears **only** on routes listed in `enabledRoutes`. On all other routes (e.g. splash, login, settings) it is hidden automatically. Alternatively, to show the agent on **every** screen except a few: ```dart embedEvent( EventKeys.CUSTOM_EVENT, CustomEventPayload( data: { 'context': 'pricing_screen', 'event': 'option_selected', 'option_id': 'plan_12_months', 'price_per_month': 249, 'currency': 'INR', }, ), ); ``` {/* > **Tip**: Combine `CUSTOM_EVENT` with your flow configuration to build rich analytics around user journeys. */} #### Available Events [#available-events] | Event | Purpose | Required | When to Use | | -------------- | ------------------------------------------------ | ------------ | -------------------------------------------------------------- | | `USER_DATA` | Initialize user context and activate EmbedWidget | **Required** | After user authentication or when user ID is available | | `SCREEN_STATE` | Provide screen context and navigation info | Optional | When navigating between screens or when screen context changes | | `CUSTOM_EVENT` | Capture bespoke user interactions | Optional | Whenever you need additional tracking for specific actions | ### Basic Usage [#basic-usage] Here's a complete example showing how to use the SDK with flow-based activation: ```dart import 'package:flutter/material.dart'; import 'package:embed_flutter/embed_flutter.dart'; void main() { embedInitialize('your-api-key', flowName: 'main'); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, enabledRoutes: const { 'main': ['product_list', 'product_detail', 'cart'], }, child: MaterialApp( // βœ… Required β€” tracks route changes navigatorObservers: [EmbedNavigatorObserver()], initialRoute: '/', routes: { '/': (ctx) => const SplashScreen(), 'home': (ctx) => const HomeScreen(), 'product_list': (ctx) => const ProductListScreen(), 'product_detail': (ctx) => const ProductDetailScreen(), 'cart': (ctx) => const CartScreen(), 'checkout': (ctx) => const CheckoutScreen(), }, ), ); } } ``` **Navigating between routes:** ```dart // Push Navigator.pushNamed(context, 'product_list'); // Push with arguments Navigator.pushNamed(context, 'product_detail', arguments: {'id': 42}); // Replace (useful for tabs β€” see section 5.5) Navigator.pushReplacementNamed(context, 'cart'); ``` `EmbedNavigatorObserver` intercepts every push, pop, and replace and shows or hides the agent button based on whether the new route is in `enabledRoutes`. *** ### 5.2 MaterialApp with onGenerateRoute [#52-materialapp-with-ongenerateroute] When routes carry parameters you typically use `onGenerateRoute`. Everything else is identical. ```dart class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, enabledRoutes: const { 'main': ['welcome', 'personal_info', 'address', 'review'], }, child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], // βœ… required home: const SplashScreen(), onGenerateRoute: (settings) { switch (settings.name) { case 'welcome': return MaterialPageRoute( settings: settings, // ← always pass settings so the observer reads the name builder: (_) => const WelcomeScreen(), ); case 'personal_info': final args = settings.arguments as Map?; return MaterialPageRoute( settings: settings, builder: (_) => PersonalInfoScreen(data: args), ); case 'address': return MaterialPageRoute( settings: settings, builder: (_) => const AddressScreen(), ); case 'review': return MaterialPageRoute( settings: settings, builder: (_) => const ReviewScreen(), ); default: return null; } }, ), ); } } ``` Always pass `settings: settings` to `MaterialPageRoute`. Without it the route name is `null` and the SDK cannot determine whether to show the agent. *** ### 5.3 GoRouter [#53-gorouter] GoRouter uses the `name` field of each `GoRoute` for matching. Pass the observer in `GoRouter.observers`. ```dart import 'package:go_router/go_router.dart'; import 'package:embed_flutter/embed_flutter.dart'; // ── Router definition ───────────────────────────────────────────────────────── final _router = GoRouter( initialLocation: '/splash', // βœ… Add observer here observers: [EmbedNavigatorObserver()], routes: [ GoRoute( path: '/splash', name: 'splash', builder: (context, state) => const SplashScreen(), ), GoRoute( path: '/home', name: 'home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:id', name: 'product_detail', builder: (context, state) { final id = state.pathParameters['id']!; return ProductDetailScreen(id: id); }, ), GoRoute( path: '/cart', name: 'cart', builder: (context, state) => const CartScreen(), ), GoRoute( path: '/payment', name: 'payment', builder: (context, state) => const PaymentScreen(), ), ], ); // ── main ────────────────────────────────────────────────────────────────────── void main() { embedInitialize('your-api-key', flowName: 'main'); runApp(const MyApp()); } // ── App widget ──────────────────────────────────────────────────────────────── class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, // Use the `name:` field from GoRoute β€” not the path enabledRoutes: const { 'main': ['home', 'product_detail', 'cart', 'payment'], }, child: MaterialApp.router( routerConfig: _router, ), ); } } ``` **Navigating with GoRouter:** ```dart // By name context.goNamed('cart'); // By path context.go('/cart'); // Push (adds to stack) context.pushNamed('product_detail', pathParameters: {'id': '42'}); ``` #### GoRouter with `nameExtractor` (path-based matching) [#gorouter-with-nameextractor-path-based-matching] If you prefer to match on URL paths instead of route names, supply a `nameExtractor` to the observer: ```dart EmbedNavigatorObserver( nameExtractor: (route) { final name = route.settings.name ?? ''; if (name.startsWith('/product/')) return 'product_detail'; if (name == '/cart') return 'cart'; return null; // null = SDK ignores this push }, ) ``` Then use those extracted names in `enabledRoutes` as usual. *** ### 5.4 GoRouter with ShellRoute (Bottom Tab Navigator) [#54-gorouter-with-shellroute-bottom-tab-navigator] `ShellRoute` keeps a persistent shell (e.g. a bottom navigation bar) while swapping child routes. The challenge is that `ShellRoute` children run inside a **nested navigator** β€” the top-level observer does not fire for them. **Solution:** Wrap each tab's screen with `EmbedRouteListener`. This widget notifies the SDK of the current screen whenever the tab is displayed. ```dart import 'package:go_router/go_router.dart'; import 'package:embed_flutter/embed_flutter.dart'; // ── Router ──────────────────────────────────────────────────────────────────── final _router = GoRouter( initialLocation: '/home', observers: [EmbedNavigatorObserver()], // catches pushes outside the shell routes: [ ShellRoute( builder: (context, state, child) => AppShell(child: child), routes: [ GoRoute( path: '/home', name: 'home', builder: (context, state) => const HomeTab(), ), GoRoute( path: '/offers', name: 'offers', builder: (context, state) => const OffersTab(), ), GoRoute( path: '/profile', name: 'profile', builder: (context, state) => const ProfileTab(), ), // Sub-route inside a tab (pushed onto the nested navigator) GoRoute( path: '/offers/detail/:id', name: 'offer_detail', builder: (context, state) { final id = state.pathParameters['id']!; return OfferDetailScreen(id: id); }, ), ], ), ], ); // ── Shell scaffold ──────────────────────────────────────────────────────────── class AppShell extends StatelessWidget { final Widget child; const AppShell({super.key, required this.child}); @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri.toString(); int currentIndex = 0; if (location.startsWith('/offers')) currentIndex = 1; if (location.startsWith('/profile')) currentIndex = 2; return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, onTap: (i) { switch (i) { case 0: context.go('/home'); break; case 1: context.go('/offers'); break; case 2: context.go('/profile'); break; } }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.local_offer), label: 'Offers'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ], ), ); } } // ── Tab screens ─────────────────────────────────────────────────────────────── // Wrap each tab with EmbedRouteListener so the SDK knows which tab is active. class HomeTab extends StatelessWidget { const HomeTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'home', // βœ… must match the name in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Home')), body: const Center(child: Text('Home content')), ), ); } } class OffersTab extends StatelessWidget { const OffersTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'offers', // βœ… agent visible β€” listed in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Offers')), body: const Center(child: Text('Browse offers')), ), ); } } class ProfileTab extends StatelessWidget { const ProfileTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'profile', // agent hidden β€” not listed in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Profile')), body: const Center(child: Text('Your profile')), ), ); } } // ── main ────────────────────────────────────────────────────────────────────── void main() { embedInitialize('your-api-key', flowName: 'main'); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, enabledRoutes: const { 'main': ['home', 'offers', 'offer_detail'], // 'profile' is intentionally excluded β€” agent hidden there }, child: MaterialApp.router(routerConfig: _router), ); } } ``` **Key rules for ShellRoute:** | Rule | Why | | ------------------------------------------------------------------ | ------------------------------------------------------------------ | | Add `EmbedNavigatorObserver()` to the top-level `GoRouter` | Catches pushes onto the root navigator (screens outside the shell) | | Wrap each tab's screen with `EmbedRouteListener(routeName: '...')` | Tells the SDK which tab is currently visible | | The `routeName` must match what you put in `enabledRoutes` | Otherwise the SDK cannot determine visibility | *** ### 5.5 MaterialApp Bottom TabNavigator (BottomNavigationBar) [#55-materialapp-bottom-tabnavigator-bottomnavigationbar] When using `BottomNavigationBar` with a plain `MaterialApp`, push a new named route for each tab using `Navigator.pushReplacementNamed` so the route stack stays shallow and the observer fires on every tab switch. ```dart // ── main.dart ───────────────────────────────────────────────────────────────── void main() { embedInitialize('your-api-key', flowName: 'main'); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, // Agent appears only on the Plans and Exclusive tabs enabledRoutes: const { 'main': ['plans_tab', 'exclusive_tab'], }, child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], // βœ… required home: const HomeTab(), onGenerateRoute: (settings) { switch (settings.name) { case 'home_tab': return MaterialPageRoute(settings: settings, builder: (_) => const HomeTab()); case 'plans_tab': return MaterialPageRoute(settings: settings, builder: (_) => const PlansTab()); case 'exclusive_tab': return MaterialPageRoute(settings: settings, builder: (_) => const ExclusiveTab()); case 'profile_tab': return MaterialPageRoute(settings: settings, builder: (_) => const ProfileTab()); default: return null; } }, ), ); } } // ── Shared bottom nav scaffold ──────────────────────────────────────────────── class AppNavBar extends StatelessWidget { final Widget child; final int currentIndex; const AppNavBar({super.key, required this.child, required this.currentIndex}); static const _routes = ['home_tab', 'plans_tab', 'exclusive_tab', 'profile_tab']; void _onTap(BuildContext context, int index) { if (index == currentIndex) return; // pushReplacementNamed so the back button doesn't cycle through tab history // and EmbedNavigatorObserver fires didReplace β†’ SDK updates its route state Navigator.pushReplacementNamed(context, _routes[index]); } @override Widget build(BuildContext context) { return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, type: BottomNavigationBarType.fixed, selectedItemColor: Colors.blue[700], unselectedItemColor: Colors.grey[600], onTap: (i) => _onTap(context, i), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.description), label: 'Plans'), BottomNavigationBarItem(icon: Icon(Icons.star), label: 'Exclusive'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ], ), ); } } // ── Tab screens ─────────────────────────────────────────────────────────────── class HomeTab extends StatelessWidget { const HomeTab({super.key}); @override Widget build(BuildContext context) { return AppNavBar( currentIndex: 0, // Agent hidden β€” 'home_tab' is not in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Home')), body: const Center(child: Text('Home content')), ), ); } } class PlansTab extends StatelessWidget { const PlansTab({super.key}); @override Widget build(BuildContext context) { return AppNavBar( currentIndex: 1, // βœ… Agent visible β€” 'plans_tab' is in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Plans')), body: const Center(child: Text('Browse plans')), ), ); } } class ExclusiveTab extends StatelessWidget { const ExclusiveTab({super.key}); @override Widget build(BuildContext context) { return AppNavBar( currentIndex: 2, // βœ… Agent visible β€” 'exclusive_tab' is in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Exclusive')), body: const Center(child: Text('Exclusive deals')), ), ); } } class ProfileTab extends StatelessWidget { const ProfileTab({super.key}); @override Widget build(BuildContext context) { return AppNavBar( currentIndex: 3, // Agent hidden β€” 'profile_tab' is not in enabledRoutes child: Scaffold( appBar: AppBar(title: const Text('Profile')), body: const Center(child: Text('Your profile')), ), ); } } ``` **How it works:** * `Navigator.pushReplacementNamed` fires `didReplace` on `EmbedNavigatorObserver`, which updates the current route in the SDK. * The SDK checks the new route name against `enabledRoutes` and shows or hides the agent button β€” no extra code needed. *** ## 6. Sending Events [#6-sending-events] ### 6.1 USER\_DATA (Required) [#61-user_data-required] This is the **only required event**. Send it after you have a logged-in user ID. It activates voice features and associates all future analytics with that user. ```dart embedEvent( EventKeys.USER_DATA, UserEventPayload( app_user_id: 'user_12345', // required β€” your unique user identifier data: { 'name': 'Priya Sharma', // optional extra fields sent to the agent 'email': 'priya@example.com', 'plan': 'premium', }, ), ); ``` **When to call it:** | Scenario | When to call | | ------------------- | ----------------------------------------------------- | | Auth flow | Immediately after `signIn()` / token refresh succeeds | | No auth (demo apps) | In `initState` of your first meaningful screen | The SDK queues this event internally if the server config hasn't loaded yet and flushes it automatically β€” you do not need to wait or retry. *** ### 6.2 SCREEN\_STATE (Optional) [#62-screen_state-optional] Sends additional context about the current screen to the agent. Useful when you want the agent to know which step of a multi-step form the user is on, or when multiple routes share one screen widget. ```dart embedEvent( EventKeys.SCREEN_STATE, ScreenEventPayload( screen: 'loan_application_step_2', data: { 'step': 2, 'total_steps': 5, 'category': 'home_loan', }, ), ); ``` Call it in `initState` or whenever meaningful context changes (e.g. a stepper advances). *** ### 6.3 CUSTOM\_EVENT (Optional) [#63-custom_event-optional] Captures any interaction you want to relay to the agent β€” button taps, option selections, modal opens, etc. ```dart embedEvent( EventKeys.CUSTOM_EVENT, CustomEventPayload( data: { 'context': 'plan_selection', 'action': 'plan_selected', 'plan_id': 'gold_12m', 'price': 1499, 'currency': 'INR', }, ), ); ``` *** ### 6.4 ANALYTICS\_DATA (Optional) [#64-analytics_data-optional] Like `CUSTOM_EVENT` but requires a named event identifier. Use this for conversion events, funnel steps, or any event you need to categorise by a fixed name on the backend. ```dart embedEvent( EventKeys.ANALYTICS_DATA, AnalyticsDataEventPayload( event_name: 'payment_completed', data: { 'amount': 4999, 'currency': 'INR', 'payment_method': 'upi', 'transaction_id': 'txn_abc123', }, ), ); ``` **Event reference table:** | Event | Class | `event_name` required? | Typical use | | ---------------- | --------------------------- | ---------------------- | --------------------------------------------- | | `USER_DATA` | `UserEventPayload` | β€” | Identifying the user and activating the agent | | `SCREEN_STATE` | `ScreenEventPayload` | β€” | Providing screen or step context to the agent | | `CUSTOM_EVENT` | `CustomEventPayload` | No | Ad-hoc interactions and selections | | `ANALYTICS_DATA` | `AnalyticsDataEventPayload` | **Yes** | Named funnel and conversion events | *** ## 7. EmbedWidget Parameters Reference [#7-embedwidget-parameters-reference] | Parameter | Type | Default | Description | | --------------------------- | --------------------------- | ---------------------- | ------------------------------------------------------------------------------- | | `child` | `Widget` | β€” | **Required.** Your `MaterialApp` / `MaterialApp.router` | | `showEmbedWidget` | `bool` | `true` | Master visibility toggle β€” set to `false` to hide the agent everywhere | | `enabledRoutes` | `Map>` | `{}` | Route names where the agent button should appear | | `showOnAllRoutes` | `bool` | `false` | Show on every route. Use with `disabledRoutes` to exclude specific screens | | `disabledRoutes` | `List` | `[]` | Routes where the agent is always hidden (takes priority over `showOnAllRoutes`) | | `routeMatchMode` | `RouteMatchMode` | `exact` | How route names are compared β€” see section 7.1 | | `apiKey` | `String?` | from `embedInitialize` | Override API key directly on the widget | | `embedUrl` | `String?` | from `embedInitialize` | Override server base URL | | `rightPadding` | `double?` | server config | FAB distance from the right edge in logical pixels | | `bottomPadding` | `double?` | server config | FAB distance from the bottom edge in logical pixels | | `onPermissionStatusChanged` | `void Function(bool)?` | β€” | Called after the mic permission dialog β€” `true` = granted, `false` = denied | For per-flow visibility props β€” `embedButtonDelayMs`, `groupDelays`, `groupContinuity`, and `groupInsets` β€” see **[EmbedWidget advanced](/embed/integration/embed-flutter-advance)**. ### 7.1 Route Match Modes [#71-route-match-modes] By default route names must match **exactly**. Use `routeMatchMode` to relax this: ```dart EmbedWidget( routeMatchMode: RouteMatchMode.startsWith, enabledRoutes: const { 'main': ['/product'], // matches /product, /product/detail, /product/42, etc. }, child: ..., ) ``` | Mode | Behaviour | Example pattern | Matches | | --------------------------- | ----------------------------------- | --------------- | ---------------------------------------------------- | | `RouteMatchMode.exact` | Exact string match (default) | `'cart'` | only `'cart'` | | `RouteMatchMode.startsWith` | Route starts with the pattern | `'/product'` | `/product`, `/product/detail`, `/product/42` | | `RouteMatchMode.contains` | Route contains the pattern anywhere | `'product'` | `'product_list'`, `'new_product'`, `'/v2/product/3'` | Leading slashes are normalised β€” `'home'` and `'/home'` are treated identically. *** ## 8. Advanced: Widget Visibility and Positioning [#8-advanced-widget-visibility-and-positioning] This section covers the common cases β€” custom position, show-on-all, and the mic callback. For **route flows/groups**, **per-flow show delays + policies**, **continuity**, and **per-flow insets**, see the companion guide: **Important for production UX.** Read it once you move past a simple `enabledRoutes` list and need the FAB to behave differently per flow or screen. ### Custom FAB Position [#custom-fab-position] Override the default floating button position with `rightPadding` and `bottomPadding`: ```dart EmbedWidget( showEmbedWidget: true, enabledRoutes: const { 'main': ['home', 'plans', 'cart'], }, rightPadding: 20.0, // 20px from the right edge bottomPadding: 100.0, // 100px from the bottom edge child: MaterialApp( navigatorObservers: [EmbedNavigatorObserver()], home: const HomeScreen(), ), ) ``` ### Show on All Routes with Exclusions [#show-on-all-routes-with-exclusions] When you want the agent everywhere except a handful of screens: ```dart EmbedWidget( showEmbedWidget: true, showOnAllRoutes: true, disabledRoutes: const ['splash', 'login', 'otp_verification'], child: ..., ) ``` ### Microphone Permission Callback [#microphone-permission-callback] Handle the case where the user denies microphone access when starting a call: ```dart final _messengerKey = GlobalKey(); EmbedWidget( showEmbedWidget: true, enabledRoutes: const { 'main': ['home', 'dashboard'], }, onPermissionStatusChanged: (bool granted) { if (!granted) { _messengerKey.currentState?.showSnackBar( const SnackBar( content: Text( 'Microphone access is required for voice calls. Please enable it in Settings.', ), ), ); } }, child: MaterialApp( scaffoldMessengerKey: _messengerKey, navigatorObservers: [EmbedNavigatorObserver()], home: const HomeScreen(), ), ) ``` **When is `onPermissionStatusChanged` called?** * After the user taps the agent button and the permission dialog is shown. * Before the call connection is established. * `true` = permission granted, the call will proceed. * `false` = permission denied or permanently denied. *** ## 9. Advanced: Listening to SDK Events [#9-advanced-listening-to-sdk-events] Subscribe to events emitted by the agent from anywhere in your app. ### Agent lifecycle events [#agent-lifecycle-events] ```dart import 'package:embed_flutter/embed_flutter.dart'; class MyScreen extends StatefulWidget { ... } class _MyScreenState extends State { late final String _handle; @override void initState() { super.initState(); _handle = embedOnAgent((event) { final type = event['type'] as String; switch (type) { case SdkEventName.agentConversationStarted: print('Voice call started'); break; case SdkEventName.agentConversationEnded: print('Voice call ended'); break; case SdkEventName.microphonePermissionAllowed: print('Mic granted'); break; case SdkEventName.microphonePermissionDenied: print('Mic denied'); break; } }); } @override void dispose() { embedOffAgent(_handle); // always unsubscribe to avoid memory leaks super.dispose(); } } ``` **Available `SdkEventName` constants:** | Constant | Value | Fired when | | ------------------------------------------ | --------------------------------- | --------------------------- | | `SdkEventName.agentVisible` | `'agent_visible'` | Agent FAB appears on screen | | `SdkEventName.agentConversationStarted` | `'agent_conversation_started'` | User starts a voice call | | `SdkEventName.agentConversationEnded` | `'agent_conversation_ended'` | Voice call ends | | `SdkEventName.popupMessageVisible` | `'popup_message_visible'` | Inactivity popup is shown | | `SdkEventName.agentTapTopOpen` | `'agent_tap_top_open'` | Agent widget expanded | | `SdkEventName.agentTapTopClose` | `'agent_tap_top_close'` | Agent widget collapsed | | `SdkEventName.genToolTriggered` | `'gen_tool_triggered'` | Agent uses an AI tool | | `SdkEventName.microphonePermissionAllowed` | `'microphone_permission_allowed'` | Mic permission granted | | `SdkEventName.microphonePermissionDenied` | `'microphone_permission_denied'` | Mic permission denied | *** ## 10. Utility Functions [#10-utility-functions] | Function | Description | | --------------------------------------------------------- | ------------------------------------------------------------ | | `embedInitialize(apiKey, {flowName, embedUrl, onResult})` | Initialize the SDK before `runApp` | | `embedSetAppVersion(String version)` | Tag all events with an app version string | | `embedEvent(EventKeys, EventPayload)` | Send a typed event to the agent | | `embedTrackEvent(String key, EventPayload)` | Send an event using a raw string key | | `embedClearStorageCache()` | Wipe all persisted SDK data β€” call on logout | | `embedOnAgent(handler)` β†’ `String` | Subscribe to agent lifecycle events | | `embedOffAgent(String handle)` | Unsubscribe using the handle from `embedOnAgent` | | `EmbedWidget.isActive` | `true` when the agent FAB is visible and the session is live | | `EmbedWidget.isLive` | `true` when the server marks the agent as live | | `EmbedWidget.forceCleanup()` | Force-stop the agent and release resources | ## 11. Complete End-to-End Example [#11-complete-end-to-end-example] This example covers everything: GoRouter with a bottom tab shell, `EmbedRouteListener` for tabs, `USER_DATA` after login, `SCREEN_STATE` for sub-screen context, and `CUSTOM_EVENT` / `ANALYTICS_DATA` for interactions. ```dart // pubspec.yaml dependencies: // flutter: sdk: flutter // embed_flutter: ^0.1.0 // go_router: ^14.0.0 import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:embed_flutter/embed_flutter.dart'; // ════════════════════════════════════════════════════════════════════════════════ // main.dart // ════════════════════════════════════════════════════════════════════════════════ void main() { embedInitialize('your-api-key', flowName: 'main'); embedSetAppVersion('1.0.0'); // optional runApp(const MyApp()); } // ════════════════════════════════════════════════════════════════════════════════ // Router // ════════════════════════════════════════════════════════════════════════════════ final _router = GoRouter( initialLocation: '/login', observers: [EmbedNavigatorObserver()], routes: [ GoRoute( path: '/login', name: 'login', builder: (_, __) => const LoginScreen(), ), // Persistent tab shell ShellRoute( builder: (_, __, child) => MainShell(child: child), routes: [ GoRoute( path: '/home', name: 'home', builder: (_, __) => const HomeTab(), ), GoRoute( path: '/plans', name: 'plans', builder: (_, __) => const PlansTab(), routes: [ GoRoute( path: 'detail/:id', name: 'plan_detail', builder: (_, s) => PlanDetailScreen(id: s.pathParameters['id']!), ), ], ), GoRoute( path: '/profile', name: 'profile', builder: (_, __) => const ProfileTab(), ), ], ), // Full-screen checkout screens outside the tab shell GoRoute( path: '/cart', name: 'cart', builder: (_, __) => const CartScreen(), ), GoRoute( path: '/payment', name: 'payment', builder: (_, __) => const PaymentScreen(), ), GoRoute( path: '/confirmation', name: 'confirmation', builder: (_, __) => const ConfirmationScreen(), ), ], ); // ════════════════════════════════════════════════════════════════════════════════ // Root app widget // ════════════════════════════════════════════════════════════════════════════════ class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return EmbedWidget( showEmbedWidget: true, enabledRoutes: const { // Agent appears on these screens; hidden on login and profile 'main': ['home', 'plans', 'plan_detail', 'cart', 'payment', 'confirmation'], }, rightPadding: 16, bottomPadding: 100, onPermissionStatusChanged: (granted) { if (!granted) debugPrint('Mic permission denied'); }, child: MaterialApp.router( routerConfig: _router, theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue), ), ); } } // ════════════════════════════════════════════════════════════════════════════════ // Shell scaffold // ════════════════════════════════════════════════════════════════════════════════ class MainShell extends StatelessWidget { final Widget child; const MainShell({super.key, required this.child}); @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri.toString(); int idx = 0; if (location.startsWith('/plans')) idx = 1; if (location.startsWith('/profile')) idx = 2; return Scaffold( body: child, bottomNavigationBar: NavigationBar( selectedIndex: idx, onDestinationSelected: (i) { switch (i) { case 0: context.go('/home'); break; case 1: context.go('/plans'); break; case 2: context.go('/profile'); break; } }, destinations: const [ NavigationDestination(icon: Icon(Icons.home), label: 'Home'), NavigationDestination(icon: Icon(Icons.description), label: 'Plans'), NavigationDestination(icon: Icon(Icons.person), label: 'Profile'), ], ), ); } } // ════════════════════════════════════════════════════════════════════════════════ // Login screen β€” identify the user after sign-in // ════════════════════════════════════════════════════════════════════════════════ class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); void _login(BuildContext context) { // Your auth logic here ... // Identify the user to the SDK after successful authentication embedEvent( EventKeys.USER_DATA, UserEventPayload( app_user_id: 'user_42', data: { 'name': 'Rahul Verma', 'email': 'rahul@example.com', 'plan': 'free', }, ), ); context.go('/home'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Center( child: ElevatedButton( onPressed: () => _login(context), child: const Text('Sign In'), ), ), ); } } // ════════════════════════════════════════════════════════════════════════════════ // Tab screens β€” each wrapped with EmbedRouteListener // ════════════════════════════════════════════════════════════════════════════════ class HomeTab extends StatelessWidget { const HomeTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'home', // βœ… in enabledRoutes β€” agent visible child: Scaffold( appBar: AppBar(title: const Text('Home')), body: Center( child: ElevatedButton( onPressed: () => context.go('/cart'), child: const Text('Go to Cart'), ), ), ), ); } } class PlansTab extends StatelessWidget { const PlansTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'plans', // βœ… in enabledRoutes β€” agent visible child: Scaffold( appBar: AppBar(title: const Text('Plans')), body: ListView( children: [ ListTile( title: const Text('Gold Plan β€” β‚Ή999/mo'), onTap: () { // Send context so the agent knows which plan is being viewed embedEvent( EventKeys.SCREEN_STATE, ScreenEventPayload( screen: 'plan_detail', data: {'plan_id': 'gold', 'price': 999}, ), ); context.pushNamed('plan_detail', pathParameters: {'id': 'gold'}); }, ), ListTile( title: const Text('Platinum Plan β€” β‚Ή1999/mo'), onTap: () { embedEvent( EventKeys.SCREEN_STATE, ScreenEventPayload( screen: 'plan_detail', data: {'plan_id': 'platinum', 'price': 1999}, ), ); context.pushNamed('plan_detail', pathParameters: {'id': 'platinum'}); }, ), ], ), ), ); } } class ProfileTab extends StatelessWidget { const ProfileTab({super.key}); @override Widget build(BuildContext context) { return EmbedRouteListener( routeName: 'profile', // not in enabledRoutes β€” agent hidden child: Scaffold( appBar: AppBar(title: const Text('Profile')), body: const Center(child: Text('Your profile')), ), ); } } // ════════════════════════════════════════════════════════════════════════════════ // Plan detail screen // ════════════════════════════════════════════════════════════════════════════════ class PlanDetailScreen extends StatelessWidget { final String id; const PlanDetailScreen({super.key, required this.id}); @override Widget build(BuildContext context) { // plan_detail is a top-level GoRoute so EmbedNavigatorObserver catches the push return Scaffold( appBar: AppBar(title: Text('Plan: $id')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Plan ID: $id', style: const TextStyle(fontSize: 18)), const SizedBox(height: 24), ElevatedButton( onPressed: () { // Inform the agent the user selected a plan embedEvent( EventKeys.CUSTOM_EVENT, CustomEventPayload( data: {'action': 'plan_selected', 'plan_id': id}, ), ); context.go('/cart'); }, child: const Text('Add to Cart'), ), ], ), ), ); } } // ════════════════════════════════════════════════════════════════════════════════ // Checkout screens // ════════════════════════════════════════════════════════════════════════════════ class CartScreen extends StatelessWidget { const CartScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Cart')), body: Center( child: ElevatedButton( onPressed: () => context.go('/payment'), child: const Text('Proceed to Payment'), ), ), ); } } class PaymentScreen extends StatelessWidget { const PaymentScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Payment')), body: Center( child: ElevatedButton( onPressed: () { // Track the conversion event embedEvent( EventKeys.ANALYTICS_DATA, AnalyticsDataEventPayload( event_name: 'payment_completed', data: {'amount': 999, 'currency': 'INR', 'method': 'upi'}, ), ); context.go('/confirmation'); }, child: const Text('Pay β‚Ή999'), ), ), ); } } class ConfirmationScreen extends StatelessWidget { const ConfirmationScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Confirmed!')), body: const Center(child: Text('Your order is placed.')), ); } } ``` *** ## 12. Troubleshooting [#12-troubleshooting] ### Agent button not visible [#agent-button-not-visible] | Check | Fix | | ------------------------------------------- | -------------------------------------------------------------------------------- | | `showEmbedWidget` is `false` | Set it to `true` | | Route name is not listed in `enabledRoutes` | Add it under the `'main'` key | | `EmbedNavigatorObserver` is missing | Add it to `navigatorObservers` on `MaterialApp` or `observers` on `GoRouter` | | `USER_DATA` event not sent | The agent requires user identity. Call `embedEvent(USER_DATA, ...)` after login. | | Invalid API key | Verify the key passed to `embedInitialize` | | Network error on startup | Check the `onResult` callback in `embedInitialize` for the error message | ### Agent not showing on a specific tab (ShellRoute / BottomNavigationBar) [#agent-not-showing-on-a-specific-tab-shellroute--bottomnavigationbar] | Check | Fix | | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Tab screen not wrapped with `EmbedRouteListener` | Wrap each tab body with `EmbedRouteListener(routeName: '...')` | | `pushReplacementNamed` not used for tab switches (MaterialApp) | Switch to `pushReplacementNamed` so `didReplace` fires on `EmbedNavigatorObserver` | | Tab route name not in `enabledRoutes` | Add it to the `'main'` list | | `routeName` in `EmbedRouteListener` doesn't match `enabledRoutes` | Both must be identical strings | ### iOS microphone permission never requested [#ios-microphone-permission-never-requested] | Check | Fix | | -------------------------------------------------------- | --------------------------------------------------------- | | `NSMicrophoneUsageDescription` missing from `Info.plist` | Add the key with a usage description string | | `PERMISSION_MICROPHONE=1` missing from Podfile | Add the macro inside `post_install` and run `pod install` | ### GoRouter routes not tracked [#gorouter-routes-not-tracked] | Check | Fix | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | Observer missing | Add `EmbedNavigatorObserver()` to `GoRouter.observers` | | Using path strings in `enabledRoutes` instead of route names | Use the `name:` field value of `GoRoute`, not the `path:` string | | `nameExtractor` needed | If GoRoute `name` values differ from what you want to match, supply a custom `nameExtractor` | ### Clearing SDK data on logout [#clearing-sdk-data-on-logout] ```dart Future onLogout() async { await embedClearStorageCache(); // Navigate to login screen } ``` *** ## Related documentation [#related-documentation] Route flows/groups, per-flow show delays and policies, continuity, and per-flow insets. The companion guide for production-grade FAB behavior. *** ## Support [#support] * **Docs:** [https://docs.revrag.ai](https://docs.revrag.ai/) * **Email:** [contact@revrag.ai](mailto:contact@revrag.ai) **Last updated:** April 2026 Β· **Flutter:** 3.0.0+ Β· **`embed_flutter`:** 0.1.0 --- # iOS Native > iOS SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (Swift / SwiftUI) URL: /embed/integration/ios-native Markdown: /embed/integration/ios-native.md # iOS Integration Guide [#ios-integration-guide] **SDK version:** 1.0.0\ **Platform:** iOS (Swift / SwiftUI)\ **Min deployment target:** iOS 16.0\ **Build system:** Xcode 15+ / Swift Package Manager Get your API key from [app.revrag.ai β†’ Settings β†’ API Keys](https://app.revrag.ai). *** ## Table of Contents [#table-of-contents] 1. [Requirements](#1-requirements) 2. [How it works](#2-how-it-works) 3. [Step 1 β€” Add the package](#step-1--add-the-package) 4. [Step 2 β€” Add microphone permission](#step-2--add-microphone-permission) 5. [Step 3 β€” Initialize the SDK](#step-3--initialize-the-sdk) 6. [Step 4 β€” Identify the user](#step-4--identify-the-user) 7. [Step 5 β€” Add the floating button](#step-5--add-the-floating-button) 8. [Routing scenarios](#routing-scenarios) 9. [Button visibility control](#button-visibility-control) 10. [Events](#events) 11. [Analytics helpers](#analytics-helpers) 12. [Cleanup on logout](#cleanup-on-logout) 13. [Configuration reference](#configuration-reference) 14. [Troubleshooting](#troubleshooting) 15. [Pre-ship checklist](#pre-ship-checklist) *** ## 1. Requirements [#1-requirements] | Requirement | Value | | ------------ | --------------- | | Min OS | iOS 16.0 | | Language | Swift 5.9+ | | UI framework | SwiftUI | | Build system | Xcode 15+ / SPM | *** ## Step 1 β€” Add the package [#step-1--add-the-package] ### Swift Package Manager (recommended) [#swift-package-manager-recommended] In Xcode: **File β†’ Add Package Dependencies** Paste the URL: ``` https://github.com/RevRag-ai/embed-native ``` Select `Up to Next Major Version` from `1.0.0` β†’ select `RevragEmbed` β†’ click **Add Package**. ### CocoaPods alternative [#cocoapods-alternative] ```ruby # Podfile pod 'RevragEmbed', '~> 1.0' ``` ```bash pod install open YourApp.xcworkspace ``` *** ## Step 2 β€” Add microphone permission [#step-2--add-microphone-permission] **Required β€” your app will crash at runtime without this.** Without `NSMicrophoneUsageDescription`, iOS terminates the process the moment the SDK requests microphone access. This step cannot be skipped. Add to `Info.plist`: ```xml NSMicrophoneUsageDescription This app uses the microphone to talk with the AI agent. ``` **In Xcode:** Target β†’ Info tab β†’ `+` button β†’ `Privacy - Microphone Usage Description` β†’ enter a description. *** ## Step 3 β€” Initialize the SDK [#step-3--initialize-the-sdk] Call `EmbedSDK.shared.initialize()` **once**, as early as possible. ### SwiftUI App entry point [#swiftui-app-entry-point] ```swift // YourApp.swift import SwiftUI import RevragEmbed @main struct YourApp: App { init() { Task { await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY") } } var body: some Scene { WindowGroup { ContentView() } } } ``` ### UIKit AppDelegate [#uikit-appdelegate] ```swift // AppDelegate.swift import UIKit import RevragEmbed @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Task { await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY") } return true } } ``` **What `initialize()` does:** 1. Calls `GET /embedded-agent/initialize` with your API key 2. Parses and stores your widget configuration (colors, agent name, avatar) 3. Pre-warms the Lottie animation cache 4. Installs `ClickEventTracker` for automatic rage-click detection 5. Sets `EmbedSDK.shared.isInitialized = true` on the main thread β€” the button appears automatically once true Console logs are prefixed `[RevragEmbed]` β€” check them if initialization fails. *** ## Step 4 β€” Identify the user [#step-4--identify-the-user] Call this right after your login flow completes: ```swift EmbedSDK.shared.event(.userData, data: [ "app_user_id": "user_123", // required "name": "Jane Doe", // optional "email": "jane@email.com" // optional ]) ``` Send `USER_DATA` **before** the user taps the call button. Without `app_user_id`, the agent cannot identify the user and conversation context will not be attributed. *** ## Step 5 β€” Add the floating button [#step-5--add-the-floating-button] Apply the `.embedProvider()` modifier to your **root view**. This overlays the draggable button on top of all your existing content. ```swift // ContentView.swift import SwiftUI import RevragEmbed struct ContentView: View { var body: some View { NavigationStack { HomeView() } .embedProvider( appUserId: "user_123", appVersion: Bundle.main.releaseVersionNumber ?? "1.0" ) } } ``` ### UIKit alternative [#uikit-alternative] Add `EmbedButton` as an overlay view in your root `UIViewController`: ```swift import RevragEmbed import SwiftUI // In viewDidLoad of your root UIViewController let embedView = UIHostingController(rootView: EmbedButton(appUserId: "user_123") ) embedView.view.backgroundColor = .clear addChild(embedView) view.addSubview(embedView.view) embedView.view.frame = view.bounds embedView.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] embedView.didMove(toParent: self) ``` ### `.embedProvider()` modifier props [#embedprovider-modifier-props] | Parameter | Type | Default | Description | | ---------------------- | ----------------------------- | --------------- | --------------------------------------- | | `appUserId` | `String` | `""` | Identifies the logged-in user | | `appVersion` | `String` | `"1.0.0"` | Your app's version string | | `accentColor` | `Color` | `#6C63FF` | Fallback gradient color | | `visibilityConfig` | `EmbedButtonVisibilityConfig` | show everywhere | Screen allow/exclude list and groups | | `navigationController` | `UINavigationController?` | `nil` | Enables automatic UIKit screen tracking | *** ## Routing scenarios [#routing-scenarios] Screen tracking lets the agent know which screen the user is on. Choose the scenario that matches your navigation setup. *** ### Scenario A β€” NavigationStack (most common) [#scenario-a--navigationstack-most-common] Post `EmbedViewDidAppear` from `.onAppear` on each screen. The SDK listens for this notification and updates the current screen name. ```swift struct HomeView: View { var body: some View { List { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "Home"] ) } } } struct ProductView: View { let productId: String var body: some View { ScrollView { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "ProductDetail"] ) } } } ``` Apply the provider to the root: ```swift struct ContentView: View { var body: some View { NavigationStack { HomeView() } .embedProvider(appUserId: "user_123", appVersion: "1.0") } } ``` `.onAppear` fires again when navigating back to a screen β€” this is expected and correct behavior. *** ### Scenario B β€” TabView with multiple stacks [#scenario-b--tabview-with-multiple-stacks] Apply `.embedProvider()` **outside** the `TabView` so the button floats above all tabs. Each tab's screens post `EmbedViewDidAppear` from `.onAppear` as shown in Scenario A. ```swift struct ContentView: View { var body: some View { TabView { NavigationStack { HomeView() } .tabItem { Label("Home", systemImage: "house") } NavigationStack { ExploreView() } .tabItem { Label("Explore", systemImage: "magnifyingglass") } NavigationStack { ProfileView() } .tabItem { Label("Profile", systemImage: "person") } } // Provider goes here β€” outside TabView β€” so button floats over all tabs .embedProvider(appUserId: "user_123", appVersion: "1.0") } } ``` Do **not** place `.embedProvider()` inside a tab. If it's scoped to one tab the button will disappear when switching to other tabs. *** ### Scenario C β€” No NavigationStack (flat views / custom transitions) [#scenario-c--no-navigationstack-flat-views--custom-transitions] Fire the notification manually when your view becomes visible. You can also post `EmbedViewDidDisappear` on exit. ```swift struct CheckoutView: View { var body: some View { VStack { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "Checkout"] ) } .onDisappear { // Optional β€” fires SCREEN_STATE with action: "exit" NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidDisappear"), object: nil, userInfo: ["screen": "Checkout"] ) } } } ``` *** ### Scenario D β€” UIKit UINavigationController [#scenario-d--uikit-uinavigationcontroller] Pass the `UINavigationController` to `.embedProvider()` for automatic tracking: ```swift struct ContentView: View { let navController: UINavigationController var body: some View { UIKitNavigationWrapper(navController: navController) .embedProvider( appUserId: "user_123", appVersion: "1.0", navigationController: navController // ← automatic tracking ) } } ``` The SDK observes `UINavigationControllerDelegate` and fires `SCREEN_STATE` events automatically β€” no `.onAppear` notifications needed. *** ## Button visibility control [#button-visibility-control] By default the button shows on **every screen**. Use `EmbedButtonVisibilityConfig` to control this. ### Show only on specific screens [#show-only-on-specific-screens] ```swift ContentView() .embedProvider( appUserId: "user_123", appVersion: "1.0", visibilityConfig: EmbedButtonVisibilityConfig( allowedScreens: ["Home", "Product", "Cart"], excludedScreens: ["Login", "Splash", "Onboarding"] ) ) ``` ### Keep button visible across a flow (e.g. checkout) [#keep-button-visible-across-a-flow-eg-checkout] Use a group with `.continuous` continuity so the button doesn't flash between screens: ```swift visibilityConfig: EmbedButtonVisibilityConfig( groups: [ EmbedButtonGroupConfig( id: "checkout-flow", screens: ["Cart", "Payment", "Confirmation"], continuity: .continuous, delayMs: 0.5, delayPolicy: .oncePerGroupEntry ) ] ) ``` ### Visibility rules (evaluated in priority order) [#visibility-rules-evaluated-in-priority-order] ``` 1. Screen is in excludedScreens β†’ always HIDDEN (strongest rule) 2. allowedScreens and groups are both empty β†’ always VISIBLE 3. Screen is in allowedScreens or any group β†’ VISIBLE 4. Otherwise β†’ HIDDEN ``` *** ## Events [#events] ### EventKeys reference [#eventkeys-reference] | Key | Value | Auto-fired | Host-callable | Notes | | ---------------- | ---------------- | ------------------------- | ------------- | ----------------------------------------------------------- | | `.userData` | `user_data` | No | **Yes** | Send after login; must include `app_user_id` | | `.screenState` | `screen_state` | **Yes** β€” `EmbedProvider` | Yes | Fired on every screen enter and exit | | `.analyticsData` | `analytics_data` | **Yes** β€” SDK UI | **Yes** | SDK fires built-in events; host may also fire custom events | | `.customEvent` | `custom_event` | No | Yes | Free-form host-app events | ### Sending events [#sending-events] ```swift // User identity (required before first call) EmbedSDK.shared.event(.userData, data: ["app_user_id": "user_123"]) // Custom analytics event EmbedSDK.shared.event(.analyticsData, data: ["event_name": "checkout_started"]) // Manual screen state EmbedSDK.shared.event(.screenState, data: ["screen": "CheckoutScreen", "action": "enter"]) ``` ### Listen for agent call events [#listen-for-agent-call-events] ```swift import RevragEmbed class CallManager { private var connectedId: UUID? private var disconnectedId: UUID? func startListening() { connectedId = EmbedSDK.shared.onAgent(.agentConnected) { _ in // e.g. pause background audio print("Call started") } disconnectedId = EmbedSDK.shared.onAgent(.agentDisconnected) { payload in let duration = (payload["metadata"] as? [String: Any])?["callDuration"] as? Int ?? 0 print("Call lasted \(duration)s") } } func stopListening() { if let id = connectedId { EmbedSDK.shared.offAgent(.agentConnected, id: id) } if let id = disconnectedId { EmbedSDK.shared.offAgent(.agentDisconnected, id: id) } } } ``` ### Agent lifecycle events reference [#agent-lifecycle-events-reference] | Event | Payload | Use case | | ---------------------- | ----------------------------------------------------- | ------------------------------------- | | `.agentConnected` | `timestamp`, `metadata.callDuration: 0`, `server_url` | Pause background audio, start a timer | | `.agentDisconnected` | `timestamp`, `metadata.callDuration` (seconds) | Resume audio, log call length | | `.popupMessageVisible` | β€” | Track tooltip impressions | *** ## Analytics helpers [#analytics-helpers] These helpers fire `analyticsData` events with standardized payloads. ```swift // Track a custom event EmbedSDK.shared.trackEvent("product_viewed", properties: ["product_id": "SKU-123"]) // Track a form interaction EmbedSDK.shared.trackFormEvent( formId: "checkout_form", eventType: "submit", formData: ["step": "payment"] ) // Track a rage-click EmbedSDK.shared.trackRageClick( elementId: "btn_add_to_cart", coordinates: CGPoint(x: 200, y: 450), clickCount: 5, elementType: "UIButton" ) // Check microphone permission status EmbedSDK.shared.checkMicPermission { granted in if !granted { self.showMicEducationAlert() } } ``` ### Events auto-fired by the SDK [#events-auto-fired-by-the-sdk] | `event_name` | Trigger | | ----------------------------- | ------------------------------------------------------------------ | | `agent_tap_to_open` | User taps the collapsed FAB | | `agent_tap_to_close` | User taps avatar to close the card | | `agent_visible` | `EmbedProvider` show-delay completes | | `popup_message_visible` | Inactivity nudge fires the tooltip | | `gen_tool_triggered` | First data-channel message received | | `agent_conversation_started` | LiveKit `connect()` succeeds | | `microphone_permission_allow` | On every call start (`status: already_granted` or `newly_granted`) | *** ## Cleanup on logout [#cleanup-on-logout] Call this when the user logs out or switches accounts to prevent stale data from leaking into the next session. ```swift func onUserLogout() { EmbedSDK.shared.clearStorageCache() } ``` *** ## Configuration reference [#configuration-reference] ### `EmbedButtonVisibilityConfig` [#embedbuttonvisibilityconfig] | Field | Type | Default | Description | | ----------------- | -------------------------- | ------------------ | ----------------------------------------------- | | `allowedScreens` | `[String]` | `[]` (all screens) | Button shows only on these screens | | `excludedScreens` | `[String]` | `[]` | Button always hidden on these screens | | `showDelay` | `TimeInterval` s | `0` | Delay before button fades in on allowed screens | | `groups` | `[EmbedButtonGroupConfig]` | `[]` | Per-group override rules | | `defaultInset` | `EmbedButtonInset?` | SDK default | Default edge snap position | ### `EmbedButtonGroupConfig` [#embedbuttongroupconfig] | Field | Type | Default | Description | | ------------- | ------------------------ | ------------ | ------------------------------------------------------------------------- | | `id` | `String` | required | Unique group identifier | | `screens` | `[String]` | required | Screens that belong to this group | | `continuity` | `EmbedButtonContinuity` | `.perScreen` | `.continuous` keeps the button mounted across group screens without flash | | `inset` | `EmbedButtonInset?` | `nil` | Per-group snap position override | | `delayMs` | `TimeInterval` s | `0` | Show delay for this group | | `delayPolicy` | `EmbedButtonDelayPolicy` | `.perScreen` | Controls when the delay resets | ### `EmbedButtonDelayPolicy` [#embedbuttondelaypolicy] | Value | Behaviour | | -------------------- | -------------------------------------------------- | | `.perScreen` | Delay applies every time the screen becomes active | | `.oncePerGroupEntry` | Delay only on the first entry into a group | | `.oncePerAppSession` | Delay only on the very first view of the session | ### `EmbedButton` direct props [#embedbutton-direct-props] | Parameter | Type | Default | Description | | ------------- | ------------------ | ------------------- | ----------------------------------------- | | `appUserId` | `String` | `""` | User identifier passed to the voice agent | | `accentColor` | `Color` | `#6C63FF` | Fallback gradient | | `isVisible` | `Bool` | `true` | Hides the button without unmounting it | | `inset` | `EmbedButtonInset` | right=24, bottom=80 | Snap position from screen edges | *** ## Troubleshooting [#troubleshooting] ### App crashes on first call [#app-crashes-on-first-call] **Error:** ``` This app has crashed because it attempted to access privacy-sensitive data without a usage description. ``` **Fix:** Add `NSMicrophoneUsageDescription` to `Info.plist`. See [Step 2](#step-2--add-microphone-permission) above. *** ### The button never appears [#the-button-never-appears] **Cause A β€” `initialize()` not called or failed** Check the Xcode console for `[RevragEmbed]` prefixed logs: ``` [RevragEmbed] πŸš€ Stage 1 β€” initialize() called [RevragEmbed] 🌐 Stage 3 β€” calling GET /embedded-agent/initialize … [RevragEmbed] ❌ Initialization failed: ``` Fix: verify your API key has no leading/trailing spaces and the device has internet. *** **Cause B β€” SDK not ready yet** `initialize()` is async. The button is hidden until `isInitialized = true` and appears automatically β€” no action required. To observe readiness: ```swift print(EmbedSDK.shared.isInitialized) // should be true ``` *** **Cause C β€” Screen excluded by visibility config** Screen names are **case-sensitive** and must match exactly what you post in `EmbedViewDidAppear` notifications. ```swift // What you post in .onAppear: userInfo: ["screen": "ProductDetail"] // Must match exactly in visibilityConfig: allowedScreens: ["Home", "ProductDetail"] // βœ… correct allowedScreens: ["Home", "productdetail"] // ❌ case mismatch allowedScreens: ["Home", "Product Detail"] // ❌ space mismatch ``` *** ### Button disappears when switching tabs [#button-disappears-when-switching-tabs] **Cause:** `.embedProvider()` is applied inside a tab instead of outside the `TabView`. ```swift // ❌ Wrong β€” provider scoped to one tab only TabView { HomeView() .embedProvider(appUserId: "user_123") // hidden on all other tabs .tabItem { ... } } // βœ… Correct β€” provider wraps the entire TabView TabView { HomeView().tabItem { ... } ProfileView().tabItem { ... } } .embedProvider(appUserId: "user_123") // visible on all tabs ``` *** ### Screen tracking not working (agent doesn't know current screen) [#screen-tracking-not-working-agent-doesnt-know-current-screen] **Cause:** `EmbedViewDidAppear` notification not posted from `.onAppear`. ```swift // βœ… Add this to every screen's .onAppear .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "YourScreenName"] ) } ``` *** ### Background music doesn't resume after a call [#background-music-doesnt-resume-after-a-call] The SDK calls `AVAudioSession.setActive(false, options: .notifyOthersOnDeactivation)` automatically in `endCall()`. If you observe this issue, ensure you are on SDK version β‰₯ 1.0 and that the session is not being deactivated before the SDK finishes cleaning up. *** ### Analytics events missing from dashboard [#analytics-events-missing-from-dashboard] Check in this order: 1. `USER_DATA` was sent with a valid `app_user_id` before other events 2. `initialize()` completed successfully (`isInitialized == true`) 3. Device has internet connectivity 4. The SDK rate-limits to 5 req/s β€” bursts are **queued, not dropped** *** ### User identity leaks between accounts [#user-identity-leaks-between-accounts] Always call `clearStorageCache()` on logout before the next user logs in: ```swift func onUserLogout() { EmbedSDK.shared.clearStorageCache() } ``` *** ## Pre-ship checklist [#pre-ship-checklist] ### Basic setup [#basic-setup] * [ ] Package added via Xcode SPM: `https://github.com/RevRag-ai/embed-native` * [ ] `NSMicrophoneUsageDescription` added to `Info.plist` * [ ] `await EmbedSDK.shared.initialize(apiKey:)` called in `App.init()` or `AppDelegate` * [ ] `USER_DATA` event sent with `app_user_id` immediately after login * [ ] `.embedProvider()` applied to the root view (outside `TabView` if tabs are used) * [ ] `clearStorageCache()` called on logout ### Screen tracking [#screen-tracking] * [ ] `EmbedViewDidAppear` notification posted from every screen's `.onAppear` (SwiftUI NavigationStack) * [ ] Or `navigationController` passed to `.embedProvider()` for automatic UIKit tracking * [ ] Screen names in notifications match `visibilityConfig` exactly (case-sensitive) ### Visibility [#visibility] * [ ] `visibilityConfig` configured if the button should not show on all screens * [ ] `.embedProvider()` placed outside `TabView` (if applicable) ### Production readiness [#production-readiness] * [ ] `[RevragEmbed]` logs checked β€” no initialization errors * [ ] Agent event listeners started and stopped at appropriate lifecycle points * [ ] Tested on a physical device β€” microphone permission dialog does not appear in Simulator *** ## Support [#support] * Issues: [GitHub Issues](https://github.com/RevRag-ai/embed-native/issues) * Docs: [Revrag Documentation](https://docs.revrag.ai) * Dashboard: [app.revrag.ai](https://app.revrag.ai) --- # NodeJS > Integrate RevRag's embedded agent into your NodeJS applications URL: /embed/integration/nodejs Markdown: /embed/integration/nodejs.md # NodeJS Integration [#nodejs-integration]

🚧 Coming Soon

NodeJS integration is currently in development and will be available soon!

## What to Expect [#what-to-expect] Our NodeJS integration will provide: * **Server-side SDK** for backend integrations * **Express.js middleware** for easy setup * **WebSocket support** for real-time interactions * **TypeScript definitions** for better developer experience * **Comprehensive API documentation** with examples ## Current Status [#current-status] We're building a robust NodeJS experience that will include: * βœ… Core architecture design completed * βœ… API specification finalized * 🚧 SDK implementation in progress * 🚧 Express.js middleware development * 🚧 Testing and performance optimization * 🚧 Documentation and code samples ## Use Cases [#use-cases] Our NodeJS integration will be perfect for: * **Backend API integrations** - Add AI capabilities to your REST APIs * **Real-time chat applications** - Power chatbots and conversational interfaces * **Data processing pipelines** - Integrate AI into your server workflows * **Microservices architectures** - Deploy as standalone AI services * **Webhook processors** - Handle AI requests in serverless environments ## Get Notified [#get-notified] Stay updated on our NodeJS integration progress: * Follow us on [Twitter/X](https://x.com/revrag_ai) for development updates * Star our [GitHub repository](https://github.com/revrag) to track releases * Subscribe to our newsletter for major announcements ## Technical Preview [#technical-preview] Here's a preview of what the API might look like: ```javascript const { RevRagAgent } = require('@revrag/nodejs-sdk'); const agent = new RevRagAgent({ apiKey: process.env.REVRAG_API_KEY, environment: 'production' }); // Express.js middleware example app.use('/ai', agent.middleware()); // Direct API usage const response = await agent.query({ message: "Hello, how can you help me?", context: userContext }); ``` ## Questions? [#questions] Interested in our NodeJS integration? We'd love to hear about your use case: * Visit our [website](https://revrag.ai) to get in touch * Join our developer community * Share your feedback and requirements *Expected release: Q2 2024* --- # React > React SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities URL: /embed/integration/react Markdown: /embed/integration/react.md # πŸš€ Complete Integration Guide [#-complete-integration-guide] ## πŸ“¦ Installation [#-installation] ```bash npm install @revrag-ai/embed-react # or yarn add @revrag-ai/embed-react ``` ## ⚠️ Important: CSS Import (REQUIRED) [#️-important-css-import-required] **The CSS file MUST be imported** for the widget to display correctly. Without it, the widget will appear unstyled. ### Option 1: Global Import (Recommended) [#option-1-global-import-recommended] **React/Vite:** ```tsx // src/main.tsx or src/index.tsx import '@revrag-ai/embed-react/style.css'; import App from './App'; import ReactDOM from 'react-dom/client'; ReactDOM.createRoot(document.getElementById('root')!).render(); ``` **Next.js App Router:** ```tsx // app/layout.tsx import '@revrag-ai/embed-react/style.css'; export default function RootLayout({ children }) { return ( {children} ); } ``` **Next.js Pages Router:** ```tsx // pages/_app.tsx import '@revrag-ai/embed-react/style.css'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { return ; } ``` ### Option 2: Component-Level Import [#option-2-component-level-import] ```tsx // YourComponent.tsx import { EmbedButton, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; ``` ### Option 3: CSS File Import [#option-3-css-file-import] ```css /* In your global styles.css */ @import '@revrag-ai/embed-react/style.css'; ``` *** ## 🎯 Basic Usage [#-basic-usage] ### Fixed Positioning (Default) [#fixed-positioning-default] ```tsx import { EmbedButton, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; function App() { const { isInitialized } = useInitialize("your-api-key"); if (!isInitialized) { return
Loading AI Assistant...
; } return (

My Application

{/* Widget will appear at bottom-right corner */}
); } ``` ### Embedded Positioning [#embedded-positioning] ```tsx import { EmbedButton, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; function HelpSection() { const { isInitialized } = useInitialize("your-api-key"); if (!isInitialized) return null; return (

Need Help?

Chat with our AI assistant

{/* Widget will appear at bottom-left of this container */}
); } ``` *** ## 🧩 EmbedProvider (Recommended) [#-embedprovider-recommended] Instead of manually placing `` on every page, wrap your app once with `EmbedProvider` and let it handle widget visibility automatically. **BEST PRACTICE:** Use `EmbedProvider` to automatically manage the EmbedButton based on the current route. The `EmbedProvider` component: * βœ… **Automatically tracks** route changes and shows/hides the widget * βœ… **Conditionally renders** the EmbedButton based on the current page * βœ… **Supports delays** before the button appears on a route * βœ… **Supports visibility groups** for per-route delay, continuity, and position * βœ… **Built-in component** - no need to create a custom provider ### Basic Setup [#basic-setup] ```tsx import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; function App() { const { isInitialized } = useInitialize("your-api-key"); if (!isInitialized) return
Loading...
; return ( {/* Your app / router */} ); } ``` ### Show on specific routes only [#show-on-specific-routes-only] ```tsx {/* Widget only visible on the listed routes */} ``` ### EmbedProvider Props [#embedprovider-props] | Property | Type | Required | Description | | ----------------------------- | -------------------------------------- | -------- | ------------------------------------------------------------ | | `children` | ReactNode | βœ… | Your app or router | | `appVersion` | string | βœ… | App version string used in analytics | | `includeScreens` | string\[] | ❌ | Routes where the button appears. Omit to show on all routes | | `matchMode` | 'exact' \| 'prefix' | ❌ | Route matching mode (default: 'exact') | | `currentPath` | string | ❌ | Current route path (auto-detected if not provided) | | `usePathHook` | () => string | ❌ | Custom hook for path detection (e.g., Next.js `usePathname`) | | `embedButtonDelayMs` | number | ❌ | Delay (ms) before the button appears on a route | | `embedButtonVisibilityConfig` | EmbedButtonVisibilityConfig | ❌ | Advanced per-group delay, continuity, and inset config | | `embedButtonPosition` | 'top' \| 'bottom' \| 'left' \| 'right' | ❌ | Button position for fixed positioning | Advanced usage guide β†’ *** ## 🎨 Responsive Behavior [#-responsive-behavior] ### Fixed Positioning [#fixed-positioning] * **Desktop (> 500px)**: Widget stays at specified position * **Mobile (≀ 500px)**: Widget expands to full width with 1rem padding from edges ### Embedded Positioning [#embedded-positioning-1] * **Wide parent (> 400px)**: Widget aligns to left/right based on `side` prop * **Narrow parent (≀ 400px)**: Widget auto-centers with equal padding *** ## πŸ”§ API Reference [#-api-reference] ### EmbedButton Props [#embedbutton-props] ```tsx interface EmbedButtonProps { // Positioning mode positioning?: 'fixed' | 'embedded'; // default: 'fixed' // Position configuration (for fixed mode) position?: { top?: string; bottom?: string; left?: string; right?: string; }; // Easy positioning (for embedded mode) side?: 'left' | 'right'; // Offset from bottom (useful for bottom navbars) bottomOffset?: number; // in pixels // Custom className className?: string; } ``` ### Hooks [#hooks] #### useInitialize [#useinitialize] ```tsx const { isInitialized, error } = useInitialize(apiKey: string); ``` Initializes the SDK with your API key. Must be called before using any other SDK features. #### useEmbed [#useembed] ```tsx const { currentPath } = useEmbed(); ``` Reads `currentPath` from `EmbedProvider` context. Useful for accessing the current route within your components. #### useLiveKit [#uselivekit] ```tsx const { connect, disconnect, toggleMute } = useLiveKit(); ``` Provides programmatic control over voice calls: * `connect()`: Initiate a voice call * `disconnect()`: End the current voice call * `toggleMute()`: Mute/unmute the microphone #### useSSRSafe / useBrowserSafe [#usessrsafe--usebrowsersafe] ```tsx const isMounted = useSSRSafe(); // or const isMounted = useBrowserSafe(); ``` SSR-safe mount detection hooks. Returns `true` when component is mounted on the client side. Useful for Next.js and other SSR frameworks to prevent hydration mismatches. *** ## πŸ“‘ Event Management [#-event-management] The SDK provides a powerful event system for tracking user data, custom events, and listening to agent state changes. ### EventKeys [#eventkeys] Available event types: ```tsx import { EventKeys } from '@revrag-ai/embed-react'; EventKeys.USER_DATA // 'user_data' - User identification and profile data EventKeys.CUSTOM_EVENT // 'custom_event' - Custom application events EventKeys.AGENT_CONNECTED // 'agent_start' - Voice agent connection (auto-tracked) EventKeys.AGENT_DISCONNECTED // 'agent_end' - Voice agent disconnection (auto-tracked) EventKeys.ANALYTICS_DATA // 'analytics_data' ``` **Note**: Only `USER_DATA` , `CUSTOM_EVENT` and `ANALYTICS_DATA` are available for manual use. Agent connection events are automatically tracked by the SDK and can be listened to via callbacks. *** ### Sending Events with embedEvent API [#sending-events-with-embedevent-api] The `embedEvent` object provides methods for sending events to track user data and custom application events. #### Send User Data [#send-user-data] ```tsx import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; // Send user data const response = await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'user@example.com', name: 'John Doe', plan: 'premium' } }); if (response.success) { console.log('User data sent successfully'); } ``` #### Send Custom Events [#send-custom-events] ```tsx import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; // Track custom application event await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', product_id: 'prod-123', amount: 99.99 } }); ``` #### Send Analytics Events [#send-analytics-events] ```tsx import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; // Track custom application event await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, data: { event_name: 'purchase_completed', // event_name is compulsory in analytics_data event product_id: 'prod-123', amount: 99.99 } }); ``` #### Event Method Signature [#event-method-signature] ```tsx embedEvent.event(params: UpdateDataRequest): Promise interface UpdateDataRequest { eventKey: EventKey; // Event type from EventKeys data: { app_user_id?: string; // User ID (required for USER_DATA) [key: string]: unknown; // Additional data }; session_id?: string; // Optional session ID } interface ApiResponse { success: boolean; data?: unknown; message?: string; error?: string; } ``` *** ### Listening to Agent Events [#listening-to-agent-events] Monitor voice agent connection status in real-time. **These events are automatically sent to your backend AND emitted locally** for you to listen to. #### Available Event Types for Listening [#available-event-types-for-listening] ```tsx import { EventKeys } from '@revrag-ai/embed-react'; // Available events for listening: EventKeys.AGENT_CONNECTED // 'agent_start' - Voice agent connected EventKeys.AGENT_DISCONNECTED // 'agent_end' - Voice agent disconnected ``` **Automatic Backend Sync:** * Agent events are **automatically sent to your backend** with `app_user_id` * Events are **also emitted locally** for real-time UI updates * Backend receives all event data including timestamps and metadata * No manual API calls needed - it's all handled automatically #### Event Listener Methods [#event-listener-methods] ```tsx import { embedEvent } from '@revrag-ai/embed-react'; // Add event listener embedEvent.addCallback(callback); // Remove event listener embedEvent.removeCallback(callback); ``` #### Basic Event Listening Example [#basic-event-listening-example] ```tsx import React, { useEffect } from 'react'; import { embedEvent, EventKeys, EmbedButton } from '@revrag-ai/embed-react'; function MyComponent() { useEffect(() => { // Define event handler const handleAgentEvent = (event) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('βœ… Agent connected:', event.data); // Update UI to show agent is available } if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('❌ Agent disconnected:', event.data); // Update UI to show agent is unavailable } }; // Register callback embedEvent.addCallback(handleAgentEvent); // Cleanup return () => { embedEvent.removeCallback(handleAgentEvent); }; }, []); return ; } ``` #### Complete Agent Monitoring Example [#complete-agent-monitoring-example] ```tsx import React, { useEffect, useState } from 'react'; import { embedEvent, EventKeys, EmbedButton } from '@revrag-ai/embed-react'; function VoiceAgentMonitor() { const [agentStatus, setAgentStatus] = useState<'idle' | 'connected' | 'disconnected'>('idle'); const [agentIdentity, setAgentIdentity] = useState(''); const [connectionTime, setConnectionTime] = useState(null); useEffect(() => { const handleEvent = (event) => { // Handle agent connection if (event.type === EventKeys.AGENT_CONNECTED) { console.log('βœ… Agent connected:', event); console.log('Identity:', event.data?.identity); console.log('Metadata:', event.data?.metadata); console.log('Timestamp:', event.timestamp); setAgentStatus('connected'); setAgentIdentity(event.data?.identity || 'Unknown'); setConnectionTime(new Date(event.timestamp)); // Update UI - show green indicator, enable features, etc. // Example: Show notification, start analytics timer } // Handle agent disconnection if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('❌ Agent disconnected:', event); console.log('Identity:', event.data?.identity); console.log('Metadata:', event.data?.metadata); console.log('Timestamp:', event.timestamp); setAgentStatus('disconnected'); // Calculate call duration if needed if (connectionTime) { const duration = Date.now() - connectionTime.getTime(); console.log('Call duration:', duration / 1000, 'seconds'); } // Update UI - show gray indicator, disable features, etc. // Example: Show feedback form, log analytics } }; // Register event listener embedEvent.addCallback(handleEvent); // Cleanup listener on unmount return () => { embedEvent.removeCallback(handleEvent); }; }, [connectionTime]); return (
Agent Status: {agentStatus}
{agentStatus === 'connected' && (

βœ“ Voice agent is active

Identity: {agentIdentity}

{connectionTime && (

Connected at: {connectionTime.toLocaleTimeString()}

)}
)}
); } ``` #### Advanced: Multiple Event Listeners [#advanced-multiple-event-listeners] ```tsx import { useEffect } from 'react'; import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; function MyApp() { useEffect(() => { // Analytics tracking const analyticsCallback = (event) => { if (event.type === EventKeys.AGENT_CONNECTED) { // Track to analytics service analytics.track('voice_agent_connected', { identity: event.data?.identity, timestamp: event.timestamp, }); } if (event.type === EventKeys.AGENT_DISCONNECTED) { analytics.track('voice_agent_disconnected', { identity: event.data?.identity, timestamp: event.timestamp, }); } }; // UI updates const uiCallback = (event) => { if (event.type === EventKeys.AGENT_CONNECTED) { showNotification('Voice agent connected'); } if (event.type === EventKeys.AGENT_DISCONNECTED) { showNotification('Voice agent disconnected'); } }; // Register multiple callbacks embedEvent.addCallback(analyticsCallback); embedEvent.addCallback(uiCallback); // Cleanup return () => { embedEvent.removeCallback(analyticsCallback); embedEvent.removeCallback(uiCallback); }; }, []); return ; } ``` #### Use Cases for Agent Events [#use-cases-for-agent-events] **AGENT\_CONNECTED:** * Show visual indicators (green dot, badge) * Enable voice-related features in UI * Start analytics timers * Update user presence status * Show notifications to user * Pause background music/media **AGENT\_DISCONNECTED:** * Update UI to show agent unavailable * Log analytics (call duration, success) * Show feedback forms * Resume background media * Clean up resources * Save conversation state #### What Gets Sent to Backend [#what-gets-sent-to-backend] When an agent event fires, the SDK automatically sends this data to your backend: ```typescript { event_id: "evt_1234567890_abc", // Unique event ID type: "agent_start", // Event type (agent_start or agent_end) app_user_id: "user-123", // Auto-added from USER_DATA session_id: "embed_session_...", // Session identifier timestamp: "2024-01-15T10:30:00Z", // ISO timestamp sdk: { sdk_name: "@revrag-ai/embed-react", sdk_version: "1.3.7", platform: "web" }, data: { identity: "agent-001", // Agent identity metadata: { // Additional metadata // ... agent-specific data } } } ``` This allows you to: * Track agent usage analytics * Monitor call durations * Understand user engagement patterns * Build reports on voice agent interactions * Audit agent connections #### Event Data Structure [#event-data-structure] ```typescript // Agent Connected Event interface AgentConnectedEvent { type: 'agent_start'; target: string; timestamp: number; // Unix timestamp data?: { identity?: string; // Agent identity metadata?: Record; }; userId?: string; sessionId: string; metadata?: Record; } // Agent Disconnected Event interface AgentDisconnectedEvent { type: 'agent_end'; target: string; timestamp: number; // Unix timestamp data?: { identity?: string; // Agent identity metadata?: Record; }; userId?: string; sessionId: string; metadata?: Record; } ``` #### Handling Connection Errors [#handling-connection-errors] ```tsx import { useEffect, useState } from 'react'; import { embed, EventKeys, EmbedButton } from '@revrag-ai/embed-react'; function AgentWithErrorHandling() { const [error, setError] = useState(null); useEffect(() => { const handleEvent = (event) => { try { if (event.type === EventKeys.AGENT_CONNECTED) { setError(null); // Handle successful connection } if (event.type === EventKeys.AGENT_DISCONNECTED) { // Check if disconnection was due to error if (event.data?.metadata?.error) { setError('Agent connection lost: ' + event.data.metadata.error); } } } catch (err) { console.error('Error handling agent event:', err); setError('Failed to process agent event'); } }; embed.addCallback(handleEvent); return () => embed.removeCallback(handleEvent); }, []); return (
{error && (
{error}
)}
); } ``` *** ## πŸ“± Mobile Optimization [#-mobile-optimization] The widget automatically adjusts for mobile devices: ### Extra Small Screens (≀ 375px) [#extra-small-screens--375px] * iPhone SE, small Android devices * Reduced padding and font sizes * Optimized button and text layouts ### Small Screens (376px - 500px) [#small-screens-376px---500px] * Standard smartphones * Balanced sizing for readability *** ## 🎯 Common Use Cases [#-common-use-cases] ### 1. Customer Support Widget with User Tracking [#1-customer-support-widget-with-user-tracking] ```tsx import { EmbedButton, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react'; import { useEffect } from 'react'; function App() { const { isInitialized } = useInitialize("your-api-key"); const currentUser = useAuthUser(); // Your auth hook useEffect(() => { if (isInitialized && currentUser) { // Send user data when user logs in embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: currentUser.id, email: currentUser.email, name: currentUser.name, subscription_tier: currentUser.plan } }); } }, [isInitialized, currentUser]); return ( <> {isInitialized && ( )} ); } ``` ### 2. E-commerce with Purchase Tracking [#2-e-commerce-with-purchase-tracking] ```tsx import { EmbedButton, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react'; function CheckoutPage() { const { isInitialized } = useInitialize("your-api-key"); const handleCheckout = async (orderData) => { // ... process order // Track purchase event await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', order_id: orderData.id, total: orderData.total, items: orderData.items.length } }); }; if (!isInitialized) return null; return (
); } ``` ### 3. Help Section Widget with Agent Status [#3-help-section-widget-with-agent-status] ```tsx import { EmbedButton, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react'; import { useState, useEffect } from 'react'; function HelpPage() { const { isInitialized } = useInitialize("your-api-key"); const [agentConnected, setAgentConnected] = useState(false); useEffect(() => { const handleAgentEvent = (event) => { if (event.type === EventKeys.AGENT_CONNECTED) { setAgentConnected(true); } if (event.type === EventKeys.AGENT_DISCONNECTED) { setAgentConnected(false); } }; embedEvent.addCallback(handleAgentEvent); return () => embedEvent.removeCallback(handleAgentEvent); }, []); return (

Help & Support

{agentConnected && (
🟒 Agent Connected
)}
{isInitialized && ( )}
); } ``` ### 4. With Bottom Navigation [#4-with-bottom-navigation] ```tsx function MobileApp() { const { isInitialized } = useInitialize("your-api-key"); return ( <> {/* height: 60px */} {isInitialized && ( )} ); } ``` ### 5. Multi-Department Support [#5-multi-department-support] ```tsx function SupportPage() { const { isInitialized } = useInitialize("your-api-key"); if (!isInitialized) return
Loading...
; return (

Sales Support

Questions about pricing and plans

Technical Support

Help with technical issues

); } ``` ### 6. Contextual Events Based on User Actions [#6-contextual-events-based-on-user-actions] ```tsx import { embedEvent, EventKeys, EmbedButton, useInitialize } from '@revrag-ai/embed-react'; function ProductPage({ product }) { const { isInitialized } = useInitialize("your-api-key"); const handleAddToCart = async () => { // Send custom event when user adds to cart await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'product_added_to_cart', product_id: product.id, product_name: product.name, price: product.price } }); }; return (
{isInitialized && }
); } ``` *** ## πŸ”„ Complete Integration Example [#-complete-integration-example] Here's a complete example showing initialization, user tracking, event listening, and the widget all working together: ```tsx import React, { useEffect, useState } from 'react'; import { EmbedButton, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; function App() { const { isInitialized, isLoading, error } = useInitialize("your-api-key"); const [agentStatus, setAgentStatus] = useState('idle'); const [userDataSent, setUserDataSent] = useState(false); // Initialize user data when SDK is ready useEffect(() => { if (isInitialized && !userDataSent) { initializeUserData(); } }, [isInitialized, userDataSent]); // Listen to agent events useEffect(() => { const handleAgentEvent = (event) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('Agent connected:', event); setAgentStatus('connected'); // Show notification, enable features, etc. } if (event.type === EventKeys.AGENT_DISCONNECTED) { console.log('Agent disconnected:', event); setAgentStatus('disconnected'); // Show feedback form, log analytics, etc. } }; embedEvent.addCallback(handleAgentEvent); return () => embedEvent.removeCallback(handleAgentEvent); }, []); const initializeUserData = async () => { try { // Send user data first (required) await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'user@example.com', name: 'John Doe', subscription: 'premium' } }); setUserDataSent(true); } catch (error) { console.error('Failed to initialize user data:', error); } }; // Send custom event on button click const handlePurchase = async () => { await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { event_name: 'purchase_completed', amount: 99.99, product_id: 'prod-123' } }); }; // Handle loading states if (isLoading) { return
Initializing AI Assistant...
; } if (error) { return
Error: {error}
; } if (!isInitialized || !userDataSent) { return
Loading...
; } return (

My Application

Agent Status: {agentStatus}
{/* AI Widget - will appear at bottom-right */}
); } export default App; ``` ### Key Points in This Example: [#key-points-in-this-example] 1. **βœ… CSS Import**: Imported at the top of the file 2. **βœ… SDK Initialization**: Using `useInitialize` hook with loading/error states 3. **βœ… User Data**: Sent first before rendering the widget 4. **βœ… Event Listeners**: Set up to monitor agent connection status 5. **βœ… Custom Events**: Tracked when user performs actions 6. **βœ… Widget Rendering**: Only rendered after successful initialization *** ## ⚑ Framework-Specific Notes [#-framework-specific-notes] ### React + Vite [#react--vite] βœ… Works perfectly with no additional configuration ### Next.js [#nextjs] βœ… Fully compatible with both App Router and Pages Router ⚠️ Import CSS in root layout or \_app file ### Create React App [#create-react-app] βœ… Works out of the box ⚠️ Import CSS in index.tsx ### Tailwind CSS Projects [#tailwind-css-projects] βœ… No conflicts! The library uses custom `embed-*` prefixed classes βœ… Your Tailwind styles won't affect the widget βœ… Widget styles won't affect your app *** ## πŸ› Troubleshooting [#-troubleshooting] ### Widget appears unstyled [#widget-appears-unstyled] **Solution**: Make sure you've imported the CSS file: ```tsx import '@revrag-ai/embed-react/style.css'; ``` ### Widget not appearing [#widget-not-appearing] **Solution**: Ensure `useInitialize` has completed: ```tsx const { isInitialized } = useInitialize("your-api-key"); if (!isInitialized) return
Loading...
; ``` ### Widget overlaps with bottom navigation [#widget-overlaps-with-bottom-navigation] **Solution**: Use the `bottomOffset` prop: ```tsx ``` ### Widget too small on mobile [#widget-too-small-on-mobile] **Solution**: This is handled automatically! The widget is fully responsive. ### Events not being sent [#events-not-being-sent] **Solution**: Ensure you've sent USER\_DATA event first with `app_user_id`: ```tsx await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', // ... other data } }); ``` ### Agent event listeners not firing [#agent-event-listeners-not-firing] **Solution**: Make sure callbacks are registered before agent connection: ```tsx useEffect(() => { const handleEvent = (event) => { // Your event handling logic }; embedEvent.addCallback(handleEvent); return () => embedEvent.removeCallback(handleEvent); }, []); ``` ### "User identity not found" error [#user-identity-not-found-error] **Solution**: Send USER\_DATA event before any other events: ```tsx // βœ… Correct order await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123' } }); await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { ... } }); // ❌ Wrong order await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { ... } }); // Error! ``` ### Custom events being blocked [#custom-events-being-blocked] **Solution**: Only `USER_DATA` and `CUSTOM_EVENT` are allowed for manual sending. Agent events (`AGENT_CONNECTED`, `AGENT_DISCONNECTED`) are auto-tracked and can only be listened to, not manually sent. *** ## πŸ“‹ Checklist [#-checklist] Before deploying, ensure: **Basic Setup:** * [ ] CSS file is imported * [ ] API key is configured * [ ] `isInitialized` is checked before rendering * [ ] Parent container has `position: relative` (for embedded mode) * [ ] Parent container has sufficient height (for embedded mode) * [ ] Bottom offset is set if you have bottom navigation **Event System:** * [ ] USER\_DATA event sent first with `app_user_id` * [ ] USER\_DATA sent before rendering EmbedButton * [ ] Event listeners registered before agent connection * [ ] Event listeners cleaned up on component unmount * [ ] Custom events include proper context (screen, flow) **Production Readiness:** * [ ] Error handling for failed event sends * [ ] Loading states during SDK initialization * [ ] Agent connection status displayed to users * [ ] Analytics tracking for agent events * [ ] Proper cleanup of callbacks on unmount *** ## πŸ†˜ Support [#-support] * πŸ“§ Issues: [GitHub Issues](https://github.com/revrag-ai/embed-react/issues) * πŸ“– Docs: [GitHub README](https://github.com/revrag-ai/embed-react) * πŸ’¬ Discussions: [GitHub Discussions](https://github.com/revrag-ai/embed-react/discussions) *** ## πŸŽ‰ You're All Set! [#-youre-all-set] The widget is now ready to use. It's: * βœ… Fully responsive * βœ… Framework-agnostic * βœ… Tailwind-compatible * βœ… Production-ready * βœ… Mobile-optimized * βœ… Real-time event tracking * βœ… Voice agent monitoring * βœ… User context aware ### Quick Reference [#quick-reference] **Import everything you need:** ```tsx import { EmbedButton, // The main widget component EmbedProvider, // Built-in provider for automatic widget management useInitialize, // SDK initialization hook embedEvent, // Event management API EventKeys, // Event type constants } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; ``` **Initialize and track:** ```tsx // 1. Initialize SDK const { isInitialized } = useInitialize("your-api-key"); // 2. Send user data await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123' } }); // 3. Listen to agent events embedEvent.addCallback((event) => { if (event.type === EventKeys.AGENT_CONNECTED) { console.log('Agent connected!'); } }); // 4. Render widget ``` Happy coding! πŸš€ --- # React Native > Step-by-step guide to integrate the RevRag React Native embed SDK (voice agent, provider, events, configuration, and native platform setup). URL: /embed/integration/react-native Markdown: /embed/integration/react-native.md # React Native embed SDK [#react-native-embed-sdk] Follow this guide in order the first time you integrate. **Native LiveKit setup is required** - skipping it is the most common source of runtime errors. *** ## Introduction [#introduction] The **`@revrag-ai/embed-react-native`** SDK adds a **voice AI agent** to your app: a **floating action button (FAB)** backed by **LiveKit**, optional **navigation-aware** visibility, and a **user-context channel** to your embed backend (`PUT .../user-context/update`). **Latest published package version:** **1.0.35** (install with `npm install @revrag-ai/embed-react-native@latest` or your package manager’s equivalent). Confirm the current version on [npm](https://www.npmjs.com/package/@revrag-ai/embed-react-native) before you pin a release in production. **What you get out of the box** * **Realtime voice** with the agent through the FAB * **Screen and app context** for richer conversations (route tracking via `EmbedProvider`, optional explicit `SCREEN_STATE`) * **Event tracking**: host-driven analytics and custom payloads via `Embed.Event`, plus **agent lifecycle** signals (`AgentEvent`) * **Best-effort click tracking** on touchables when the widget visibility rules allow it (importing the package wires this safely; failures should not crash your app) * **Server-driven UI** for the FAB via **`widget_config`** from device registration * **Advanced FAB behavior** (route **groups**, show **delays**, **insets**, per-group rules): covered in **[EmbedProvider advanced](/embed/integration/embed-provider-advanced)** - read it once you move past a simple `includeScreens` list Import only from the **package entry** (`@revrag-ai/embed-react-native`). Do not rely on deep imports from `src/` unless your team explicitly supports them. *** ## Prerequisites [#prerequisites] Before you install, confirm your environment: | Requirement | Notes | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Node.js** | 18+ recommended | | **React Native** | 0.70 or higher | | **iOS** | iOS 13+ | | **Android** | API 21+ | | **Navigation** | `@react-navigation/native` is the typical setup for `EmbedProvider` (optional if you mount `EmbedButton` manually) | | **Embed package** | **`@revrag-ai/embed-react-native`** β€” latest **1.0.35** on npm ([package page](https://www.npmjs.com/package/@revrag-ai/embed-react-native)); use **`@latest`** or pin a version in CI | **You will also need:** * Microphone permission (declared on both platforms; see [Appendix: Native platform setup](/embed/integration/react-native#appendix-native-platform-setup)) * **`GestureHandlerRootView`** at the app root (`react-native-gesture-handler`) * Peer libraries listed in [Installation](/embed/integration/react-native#installation) (LiveKit, Reanimated, Gesture Handler, Async Storage, Lottie, Safe Area, Linear Gradient) The SDK runs **polyfills** on import for Hermes / LiveKit safety. Audio and networking must be correctly configured or voice will fail silently or with native errors. *** ## Installation [#installation] ### Install the package [#install-the-package] ```bash npm npm install @revrag-ai/embed-react-native ``` ```bash yarn yarn add @revrag-ai/embed-react-native ``` ```bash pnpm pnpm add @revrag-ai/embed-react-native ``` ### Install peer dependencies [#install-peer-dependencies] The SDK expects these packages in your app (versions should match what the SDK release notes recommend): ```bash npm npm install @livekit/react-native @livekit/react-native-webrtc npm install @react-native-async-storage/async-storage npm install react-native-gesture-handler react-native-reanimated npm install react-native-linear-gradient lottie-react-native npm install react-native-safe-area-context cd ios && pod install && cd .. ``` ```bash yarn yarn add @livekit/react-native @livekit/react-native-webrtc @react-native-async-storage/async-storage react-native-gesture-handler react-native-reanimated react-native-linear-gradient lottie-react-native react-native-safe-area-context ``` ```bash pnpm pnpm add @livekit/react-native @livekit/react-native-webrtc @react-native-async-storage/async-storage react-native-gesture-handler react-native-reanimated react-native-linear-gradient lottie-react-native react-native-safe-area-context ``` ### Complete native setup (required) [#complete-native-setup-required] Android and iOS need **LiveKit native initialization**, permissions, Lottie on Android, Reanimated Babel config, and related steps. Those are **easy to miss** - work through [Appendix: Native platform setup](/embed/integration/react-native#appendix-native-platform-setup) once, then rebuild the app. After native steps, run: ```bash npx react-native-asset ``` *** ## Basic setup (step-by-step) [#basic-setup-step-by-step] Do these steps **in order** for a standard React Navigation app. ### Step 1 - Import the SDK [#step-1---import-the-sdk] ```tsx import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useInitialize, EmbedProvider, Embed, EmbedEventKeys } from '@revrag-ai/embed-react-native'; ``` You will also use your navigation library and (recommended) `package.json` for `appVersion`. ### Step 2 - Initialize the SDK once [#step-2---initialize-the-sdk-once] Call **`useInitialize`** near the root (for example in `App.tsx`). It registers the device, prepares LiveKit on the JS side, and returns **`{ isInitialized, error }`**. ```tsx import { useInitialize } from '@revrag-ai/embed-react-native'; export default function App() { const { isInitialized, error } = useInitialize({ apiKey: 'YOUR_EMBED_API_KEY', // embedUrl: 'https://your-embed-host', // optional; omit to use SDK default host }); if (error) { // Show an error UI or retry } if (!isInitialized) { // Optional: splash / loading until the SDK is ready } return ; } ``` ### Step 3 - Wrap the app [#step-3---wrap-the-app] 1. Wrap the whole app in **`GestureHandlerRootView`** (required for gesture handler). 2. Wrap **`NavigationContainer`** with **`EmbedProvider`**, passing the **same ref** you attach to `NavigationContainer`. The provider **mounts the FAB** for you. You usually do **not** import `EmbedButton` separately. ```tsx import { useRef } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useInitialize, EmbedProvider } from '@revrag-ai/embed-react-native'; import packageJson from './package.json'; export default function App() { const navigationRef = useRef(null); const { isInitialized, error } = useInitialize({ apiKey: 'YOUR_EMBED_API_KEY' }); if (error || !isInitialized) { return null; // replace with loading / error UI } return ( {/* navigators */} ); } ``` ### Step 4 - Configure keys and register the user [#step-4---configure-keys-and-register-the-user] | Item | Where | Purpose | | ----------------- | ----------------------------- | ---------------------------------------------------------------- | | **`apiKey`** | `useInitialize` | Authenticates your app with the embed backend | | **`embedUrl`** | `useInitialize` (optional) | Overrides the default embed API host | | **`appVersion`** | `EmbedProvider` | Sent with analytics / context (use your real app version) | | **`app_user_id`** | `Embed.Event(USER_DATA, ...)` | Stable user id after login; **required** for most backend writes | After you know the signed-in user, send **`USER_DATA`** once (or again after account switch): ```tsx await Embed.Event( EmbedEventKeys.USER_DATA, { app_user_id: user.id, data: { name: user.name, email: user.email }, }, (success, err) => { if (!success) console.warn('USER_DATA failed:', err); } ); ``` Until **`USER_DATA`** succeeds with a valid **`app_user_id`**, many **backend** updates for other event types may be skipped or cannot be built. The FAB can still render; fix registration if analytics or context look empty. *** ## Usage example [#usage-example] ### Minimal flow to verify the integration [#minimal-flow-to-verify-the-integration] 1. Finish [Appendix: Native platform setup](/embed/integration/react-native#appendix-native-platform-setup) (especially **LiveKit** `setup()` on Android and iOS). 2. Use **Step 2–3** above with a real **`apiKey`**. 3. Open a screen that is allowed by **`includeScreens`** (or omit **`includeScreens`** to allow all routes). 4. You should see the **FAB**; start a call to confirm **microphone** permission and audio. ### Larger example (navigation + `USER_DATA`) [#larger-example-navigation--user_data] This pattern waits for SDK init, registers the user with **`onResult`**, then renders navigation inside the provider. ```tsx import React, { useEffect, useRef, useState } from 'react'; import { View, StyleSheet, Text, Alert } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { NavigationContainer } from '@react-navigation/native'; import { useInitialize, EmbedProvider, Embed, EmbedEventKeys, } from '@revrag-ai/embed-react-native'; import packageJson from './package.json'; export default function App() { const navigationRef = useRef(null); const [userRegistered, setUserRegistered] = useState(false); const { isInitialized, error } = useInitialize({ apiKey: 'your_api_key_here', }); useEffect(() => { if (!isInitialized || userRegistered) return; void Embed.Event( EmbedEventKeys.USER_DATA, { app_user_id: 'user_123', data: { name: 'Test User' }, }, (success, err) => { if (success) setUserRegistered(true); else Alert.alert('Embed', 'USER_DATA failed: ' + (err ?? 'unknown')); } ); }, [isInitialized, userRegistered]); if (error) { return ( SDK error ); } if (!isInitialized || !userRegistered) { return ( Preparing embed... ); } return ( {/* Place RootNavigator here */} ); } const styles = StyleSheet.create({ centered: { flex: 1, alignItems: 'center', justifyContent: 'center' }, }); ``` ### Without React Navigation [#without-react-navigation] Mount **`EmbedButton`** yourself on screens where you want the FAB, and send **`SCREEN_STATE`** manually when the step changes. You still need **`useInitialize`**, **`GestureHandlerRootView`**, and **`USER_DATA`** for full backend behavior. *** ## Configuration options [#configuration-options] ### `useInitialize(options)` [#useinitializeoptions] | Option | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------------------------- | | **`apiKey`** | `string` | Yes | Embed API key for your app | | **`embedUrl`** | `string` | No | Override embed host; default comes from the SDK | Returns **`{ isInitialized, error }`**. Call **once** near the root. ### `EmbedProvider` props [#embedprovider-props] | Prop | Type | Required | Description | | --------------------------------- | ----------- | ----------- | --------------------------------------------------------------------------------------------------------------- | | **`children`** | `ReactNode` | Yes | Usually your `NavigationContainer` and trees below it | | **`navigationRef`** | ref | Recommended | Ref passed to `NavigationContainer` for route tracking | | **`appVersion`** | `string` | Yes | Semantic app version for analytics / context | | **`includeScreens`** | `string[]` | No | Route **names** where the FAB may show; omit or `[]` for all (subject to server config) | | **`embedButtonDelayMs`** | `number` | No | Delay before showing the FAB after a screen becomes eligible | | **`embedButtonVisibilityConfig`** | object | No | Grouped visibility, per-group delays, insets. See [advanced guide](/embed/integration/embed-provider-advanced). | ### Advanced FAB visibility and `EmbedProvider` [#advanced-fab-visibility-and-embedprovider] Basic integration uses **`includeScreens`** (and optionally **`embedButtonDelayMs`**). For **route groups**, **per-group delays**, **insets**, and **continuity** rules, you need the expanded API. Full prop tables, JSON examples, and behavior notes are in **[EmbedProvider advanced](/embed/integration/embed-provider-advanced)** - treat it as the companion doc whenever the FAB must behave differently by flow or screen. **Important for production UX.** Same guide as the **Recommended** card in **Step 3 - Wrap the app** above. Use when you configure **groups**, **delays**, and **insets**, not only **`includeScreens`**. ### Server `widget_config` (FAB look and behavior) [#server-widget_config-fab-look-and-behavior] After device registration, the SDK reads **`widget_config`** to style the FAB (avatar Lottie/image, colors, copy, corner position, paddings, nudge / inactivity behavior). **`EmbedProvider`** props control **when** the FAB is shown and delays/insets in your app; they **do not** replace **`widget_config`**. Typical top-level JSON sections map to parsed types such as **`agentAvatar`**, **`agentTextContent`**, **`colorPalette`**, **`collapsedView`** (nudge / popup), and **`position`** (corner and edge padding). Exact aliases and parsing live in the package under **`src/api/types/widget.config.types.ts`** (use that file as the backend contract). *** ## Features overview [#features-overview] | Area | What it does | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Voice session** | LiveKit realtime audio from the FAB | | **Screen context** | Provider + ref track the current route; optional **`SCREEN_STATE`** for custom steps (e.g. webviews) | | **User context API** | **`Embed.Event`** with **`USER_DATA`**, **`SCREEN_STATE`**, **`CUSTOM_EVENT`**, **`ANALYTICS_DATA`** | | **Agent lifecycle** | Local **`AgentEvent`** listeners (`embedOnAgent` / `Embed.event.on`); mirrored to backend as **`analytics_data`** with **`event_name`** | | **FAB visibility** | `includeScreens`, delays, or grouped config - see **[EmbedProvider advanced](/embed/integration/embed-provider-advanced)** for groups, insets, and delays | | **Click tracking** | Automatic on touchables when visibility rules allow | | **Mic permission** | Optional **`checkPermissions`**; listen for **`MICROPHONE_PERMISSION_ALLOWED`** / **`DENIED`** | *** ## Events and callbacks [#events-and-callbacks] There are **two separate systems**: 1. **`EmbedEventKeys` (data events)** - You call **`Embed.Event(key, data, onResult?)`**. On success, the SDK \*\*`PUT`\*\*s user-context updates with **`type`** matching the key (`user_data`, `screen_state`, `custom_event`, `analytics_data`). 2. **`AgentEvent` (agent events)** - The SDK emits these on **`Embed.event`** for FAB / voice / mic / popup moments. Subscribe with **`embedOnAgent`** or **`Embed.event.on`**. On the wire they appear as **`analytics_data`** with **`event_name`** set to the agent string. The SDK **does not** push HTTP acknowledgements back into JS. For β€œsame moment as the write”, use **local** callbacks below. ### `EmbedEventKeys` (only these four) [#embedeventkeys-only-these-four] | Key | Typical use | | -------------------- | ----------------------------------------------------------------------------------- | | **`USER_DATA`** | After login: **`app_user_id`** plus optional **`data`** object | | **`SCREEN_STATE`** | `{ screen, data? }` when you need explicit context | | **`CUSTOM_EVENT`** | Arbitrary JSON-friendly **`data`** for product events | | **`ANALYTICS_DATA`** | **`event_name` required**; optional **`data`** / **`metadata`** | **`Embed.Event` behavior (important for first-time integrators)** * The returned **promise does not reject** on HTTP failure. Always use the optional third argument **`onResult(success, error?)`** when you care about failure. * On success: **`Embed.on(key)`** handlers run, then **`onResult(true)`**. * On failure: **`Embed.on`** handlers **do not** run for that attempt. **`AgentEvent` behavior** * **`embedOnAgent`** / **`Embed.event.on`** run **before** the SDK attempts the analytics **`PUT`**. * If **`app_user_id`** is not in storage, the **HTTP mirror may be skipped**, but **listeners still ran**. ### Quick reference [#quick-reference] | Mechanism | When it runs | Notes | | ------------------------------------ | -------------------------------------- | -------------------------------------- | | **`Embed.Event(..., onResult)`** | Every data event call | Use for per-call success/failure | | **`Embed.on` / `Embed.off`** | After **successful** send for that key | Cross-cutting reactions | | **`embedOnAgent` / `embedOffAgent`** | On each **`AgentEvent` emit** | Runs even if backend mirror is skipped | | **`Embed.event.on` / `off`** | Single agent event | Remember cleanup in `useEffect` | ### Example: subscribe to all agent events [#example-subscribe-to-all-agent-events] ```tsx import { useEffect } from 'react'; import { embedOnAgent, embedOffAgent, AgentEvent } from '@revrag-ai/embed-react-native'; useEffect(() => { const handle = embedOnAgent((event) => { switch (event.type) { case AgentEvent.AGENT_CONVERSATION_STARTED: break; case AgentEvent.AGENT_CONVERSATION_ENDED: break; case AgentEvent.POPUP_MESSAGE_VISIBLE: break; case AgentEvent.MICROPHONE_PERMISSION_DENIED: break; default: break; } }); return () => embedOffAgent(handle); }, []); ``` Deprecated aliases **`AGENT_CONNECTED`** / **`AGENT_DISCONNECTED`** may still appear; prefer **`AGENT_CONVERSATION_STARTED`** / **`AGENT_CONVERSATION_ENDED`**. ### Example: after successful `SCREEN_STATE` sends [#example-after-successful-screen_state-sends] ```tsx import Embed, { EmbedEventKeys } from '@revrag-ai/embed-react-native'; const onScreen = (data: unknown) => { /* runs only after a successful API send */ }; Embed.on(EmbedEventKeys.SCREEN_STATE, onScreen); // Embed.off(EmbedEventKeys.SCREEN_STATE, onScreen); ``` ### Backend payload types (short) [#backend-payload-types-short] User-context **`PUT`** payloads use a **`type`** aligned with **`EmbedEventKeys`**. Agent lifecycle is mirrored as **`analytics_data`** with **`event_name`**. For field-level contracts, open **`BACKEND_EVENTS.md`** in **`node_modules/@revrag-ai/embed-react-native`** after install. *** ## Troubleshooting [#troubleshooting] ### `audioRecordSamplesDispatcher is not initialized!` [#audiorecordsamplesdispatcher-is-not-initialized] **Cause:** LiveKit native setup is missing. **Fix:** 1. Add **`LiveKitReactNative.setup(this)`** in Android **`MainApplication.onCreate`** before React Native starts. 2. Add **`LiveKitReactNative.setup()`** in iOS **`AppDelegate`** inside **`didFinishLaunchingWithOptions`**. 3. Clean rebuild Android / run **`pod install`** on iOS, then reinstall the app. See code snippets in [Appendix: Native platform setup](/embed/integration/react-native#appendix-native-platform-setup). ### Reanimated animations broken [#reanimated-animations-broken] **Cause:** Babel plugin order wrong. **Fix:** Put **`react-native-reanimated/plugin` last** in `babel.config.js`, then **`npx react-native start --reset-cache`**. ### β€œUser identity not found” or empty backend context [#user-identity-not-found-or-empty-backend-context] **Cause:** **`USER_DATA`** not sent or failed; other events need a stored **`app_user_id`**. **Fix:** Send **`USER_DATA`** right after login with **`onResult`**. Send other **`Embed.Event`** calls after you know registration succeeded (or handle failures explicitly). ### Microphone does not work or iOS crashes on mic access [#microphone-does-not-work-or-ios-crashes-on-mic-access] **Fix:** Android manifest needs **`RECORD_AUDIO`** (and related). iOS **`Info.plist`** must include **`NSMicrophoneUsageDescription`**. See [Appendix: Native platform setup](/embed/integration/react-native#appendix-native-platform-setup). ### FAB never appears [#fab-never-appears] **Checklist:** * **`GestureHandlerRootView`** wraps the tree * **`useInitialize`** completed without **`error`** * **`EmbedProvider`** wraps **`NavigationContainer`** and shares **`navigationRef`** * Current route name is listed in **`includeScreens`** if you set it (omit **`includeScreens`** to test β€œall routes”) ### Network / ATS errors on iOS [#network--ats-errors-on-ios] Use HTTPS in production. For development-only HTTP, add careful **`NSAppTransportSecurity`** exceptions (never ship **`NSAllowsArbitraryLoads: true`** for production). See plist examples in the appendix. ### Still stuck? [#still-stuck] Use the expandable section below for network debugging tips, or see [Support](/embed/integration/react-native#support). 1. Xcode β†’ Window β†’ Devices and Simulators β†’ open Console for the device. 2. From a machine: `curl -I https://your-api-domain.com/embedded-agent/initialize` (replace with your host). *** ## Best practices [#best-practices] * **Initialize once** at the app root with **`useInitialize`**; avoid calling it from every screen. * **Send `USER_DATA` as soon as you have a stable `app_user_id`** (typically immediately after login). Use **`onResult`** to surface failures. * **Debounce** high-frequency **`SCREEN_STATE`** or analytics calls if your navigation updates rapidly. * **Subscribe** to **`embedOnAgent`** in **`useEffect`** and **always** call **`embedOffAgent(handle)`** on cleanup (Strict Mode safe). * **Use HTTPS** and valid TLS in production; keep cleartext exceptions dev-only. * **Hide the FAB** on sensitive flows (auth, payments) with **`includeScreens`** or grouped visibility config. * **Plan FAB visibility early:** If product needs **groups**, **delays**, or **insets** beyond a flat screen list, read **[EmbedProvider advanced](/embed/integration/embed-provider-advanced)** before locking UI - retrofitting rules is harder than wiring them during integration. * **Log `event.type`** in development when integrating **`AgentEvent`**; payloads can vary by call site. *** ## Support [#support] * **Docs:** [https://docs.revrag.ai](https://docs.revrag.ai/) * **Email:** [contact@revrag.ai](mailto:contact@revrag.ai) * **Package README:** [npm](https://www.npmjs.com/package/@revrag-ai/embed-react-native) **Last updated:** April 2026 Β· **React Native:** 0.70+ Β· **`@revrag-ai/embed-react-native`:** 1.0.35 (see [npm](https://www.npmjs.com/package/@revrag-ai/embed-react-native) for newer releases) *** ## Appendix: Native platform setup [#appendix-native-platform-setup] Complete these steps on a fresh integration. They complement [Installation](/embed/integration/react-native#installation). ### LiveKit native setup (required) [#livekit-native-setup-required] Without native **`LiveKitReactNative.setup`**, voice will fail with errors such as **`audioRecordSamplesDispatcher is not initialized!`**. #### Android (`MainApplication.kt`) [#android-mainapplicationkt] ```kotlin import com.livekit.reactnative.LiveKitReactNative class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() LiveKitReactNative.setup(this) // ... } } ``` #### iOS (`AppDelegate.swift`) [#ios-appdelegateswift] ```swift import LiveKitReactNative func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { LiveKitReactNative.setup() // ... return true } ``` Then clean rebuild: ```bash cd android && ./gradlew clean && cd .. && npx react-native run-android cd ios && pod install && cd .. && npx react-native run-ios ``` ### Android manifest permissions [#android-manifest-permissions] Add to **`android/app/src/main/AndroidManifest.xml`** as children of the root **manifest** element: ```xml ``` Your **application** element may include **`android:usesCleartextTraffic="true"`** only if you truly need HTTP in dev. ### Android Lottie (`build.gradle`) [#android-lottie-buildgradle] ```groovy dependencies { implementation 'com.airbnb.android:lottie:6.0.1' } ``` #### App size (ABI splits) [#app-size-abi-splits] LiveKit increases native binary size. For production APK size, see [Android app size optimization](/embed/integration/android-app-size-optimization). ### ProGuard (Android release) [#proguard-android-release] ```text # Embed SDK -keep class com.revrag.embed.** { *; } -keep class org.webrtc.** { *; } -dontwarn org.webrtc.** # Lottie -keep class com.airbnb.lottie.** { *; } ``` ### iOS permissions (`Info.plist`) [#ios-permissions-infoplist] ```xml NSMicrophoneUsageDescription This app needs access to microphone for voice communication with AI agent NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsLocalNetworking ``` For **dev-only HTTP** to specific hosts, add **`NSExceptionDomains`** entries. Avoid **`NSAllowsArbitraryLoads: true`** in production. ### iOS pods and build settings [#ios-pods-and-build-settings] ```bash cd ios && pod install && cd .. ``` If builds fail: set **Bitcode** to **NO**, **Build Active Architecture Only** to **YES** (Debug). ### Babel (Reanimated) [#babel-reanimated] **`react-native-reanimated/plugin` must be the last plugin** in `babel.config.js`. ```javascript module.exports = { presets: ['module:@react-native/babel-preset'], plugins: [ // ...other plugins 'react-native-reanimated/plugin', ], }; ``` Then: ```bash React Native CLI npx react-native start --reset-cache ``` ```bash Expo expo start --clear ``` ### Fonts and assets [#fonts-and-assets] After native and JS setup: ```bash npx react-native-asset ``` --- # Overview > Integrate RevRag.ai's Embedded AI agents into your applications URL: /embed/introduction Markdown: /embed/introduction.md Mobile Development Light Mobile Development Dark ## Platform Integrations [#platform-integrations] Embed RevRag.ai's AI Sales and Onboarding agents directly into your applications. Our SDKs enable seamless integration across mobile and web platforms. ### Mobile Platforms [#mobile-platforms]
React Native
Cross-platform mobile integration for iOS and Android
Flutter
Voice-enabled AI agent with real-time widget tree monitoring
Android Kotlin
Native Android integration with Kotlin
iOS Swift
Coming Soon
### Web Platforms [#web-platforms]
React
Voice-enabled AI agent with real-time communication capabilities
Vue.js
Coming Soon
Angular
Voice-enabled AI agent for Angular applications
## What is the In-App agent? [#what-is-the-in-app-agent] The RevRag.ai In-App agent SDK brings our AI Sales and Onboarding capabilities directly to your applications. Embed our conversational AI power into your mobile apps and web platforms. ### Core Capabilities [#core-capabilities] * **AI-Powered Conversations** - Natural, human-like interactions for sales and onboarding * **Industry-Specific** - Pre-trained for BFSI, Fintech, and Insurtech use cases * **Conversion Optimization** - Built to drive specific business outcomes ## Getting Started [#getting-started] 1. **Choose Your Platform** - Select from our available SDKs above 2. **Get API Credentials** - Contact our team for integration keys 3. **Follow Integration Guide** - Use platform-specific documentation 4. **Configure Your Agent** - Customize for your specific use case React Native, Flutter, Android (Kotlin), React, and Angular integrations are available now. Contact [support@revrag.ai](mailto:support@revrag.ai) for access to other platforms or enterprise features. --- # Flutter WebView > Flutter WebView Integration Guide - Enable microphone access for Revrag Embed Agent in mobile apps URL: /embed/web-view-integration/flutter Markdown: /embed/web-view-integration/flutter.md {/* Flutter WebView Development Light Flutter WebView Development Dark */} # Flutter WebView Integration Guide [#flutter-webview-integration-guide] ## Overview [#overview] This Flutter WebView integration provides a solution for the microphone access challenge faced by Revrag's embed agent when implemented in mobile apps using WebView. The embed agent requires microphone access to enable voice-based AI assistance for website navigation, but traditional WebView implementations often fail to properly delegate microphone permissions to the embedded website. ## Problem Statement [#problem-statement] Revrag's embed agent is a JavaScript SDK that can be integrated into any website to provide AI-powered voice assistance. However, when mobile apps load websites containing the embed agent in a WebView, the microphone access required by the agent is often blocked or not properly delegated, preventing users from interacting with the AI assistant. ## Prerequisites [#prerequisites] * Flutter 3.0+ * iOS 13+ / Android API 21+ * Microphone permissions on target device This integration requires proper setup of WebView microphone permissions and real-time audio communication capabilities. Check out the detailed documentation for [flutter\_inappwebview](https://inappwebview.dev/docs/intro/). ## Installation [#installation] Add the required packages to your `pubspec.yaml`: ```yaml dependencies: flutter: sdk: flutter flutter_inappwebview: ^6.0.0 permission_handler: ^11.3.1 ``` Then run: ```bash flutter pub get ``` ## Solution Overview [#solution-overview] This project implements a Flutter WebView solution using the `flutter_inappwebview` package with proper microphone permission handling. The solution: 1. **Requests native microphone permissions** using the `permission_handler` package 2. **Delegates permissions to the WebView** when the embed agent requests microphone access 3. **Handles permission states** across both native and WebView contexts 4. **Provides a seamless user experience** for voice interactions ### Key Features [#key-features] * βœ… **Native Microphone Permission Handling**: Properly requests and manages microphone permissions at the app level * βœ… **WebView Permission Delegation**: Seamlessly delegates permissions to the embedded website * βœ… **Cross-Platform Support**: Works on both Android and iOS * βœ… **Error Handling**: Comprehensive error handling and user feedback * βœ… **Configurable**: Easy configuration for different websites and use cases ## Android Configuration [#android-configuration] ### 1. Android Manifest Permissions [#1-android-manifest-permissions] Add the following permissions to your `android/app/src/main/AndroidManifest.xml`: ```xml ``` ## iOS Configuration [#ios-configuration] ### 1. iOS Permissions [#1-ios-permissions] **CRITICAL:** Add the following permissions to your `ios/Runner/Info.plist`. Missing `NSMicrophoneUsageDescription` will cause the app to crash when accessing the microphone. ```xml NSMicrophoneUsageDescription This app needs access to microphone to enable voice functionality in the webview. NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsLocalNetworking ``` ## WebView Implementation [#webview-implementation] ### Permission Handling [#permission-handling] The app uses a two-layer permission system: ```dart import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; Future _requestMicrophonePermission( List resources, ) async { try { // Check current permission status PermissionStatus status = await Permission.microphone.status; if (status.isDenied) { // Request permission status = await Permission.microphone.request(); if (status.isGranted) { return PermissionResponse( resources: resources, action: PermissionResponseAction.GRANT, ); } } else if (status.isGranted) { // Permission granted, allow WebView access return PermissionResponse( resources: resources, action: PermissionResponseAction.GRANT, ); } // Permission denied return PermissionResponse( resources: resources, action: PermissionResponseAction.DENY, ); } catch (e) { print('Permission request error: $e'); return PermissionResponse( resources: resources, action: PermissionResponseAction.DENY, ); } } ``` ### WebView Configuration [#webview-configuration] The WebView is configured with specific settings to enable microphone access: ```dart InAppWebView( initialUrlRequest: URLRequest( url: WebUri('https://your-website.com') ), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: false, allowsInlineMediaPlayback: true, allowsAirPlayForMediaPlayback: true, allowsPictureInPictureMediaPlayback: true, useHybridComposition: true, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, allowFileAccess: true, allowContentAccess: true, ), onPermissionRequest: (controller, request) async { if (request.resources.contains(PermissionResourceType.MICROPHONE)) { return await _requestMicrophonePermission(request.resources); } return PermissionResponse( resources: request.resources, action: PermissionResponseAction.DENY, ); }, onLoadError: (controller, url, code, message) { print('WebView load error: $code - $message'); }, onConsoleMessage: (controller, consoleMessage) { print('Console: ${consoleMessage.message}'); }, ) ``` ## Usage Examples [#usage-examples] ### Complete Implementation Example [#complete-implementation-example] ```dart import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; class MicrophoneWebView extends StatefulWidget { final String targetUrl; const MicrophoneWebView({ Key? key, this.targetUrl = 'https://your-website.com', }) : super(key: key); @override State createState() => _MicrophoneWebViewState(); } class _MicrophoneWebViewState extends State { InAppWebViewController? _webViewController; bool _isLoading = true; bool _micGranted = false; @override void initState() { super.initState(); _requestMicrophonePermission(); } Future _requestMicrophonePermission() async { try { PermissionStatus status = await Permission.microphone.request(); setState(() { _micGranted = status.isGranted; }); } catch (e) { print('Permission request failed: $e'); _showErrorDialog('Failed to request microphone permission'); } } Future _handlePermissionRequest( List resources, ) async { if (resources.contains(PermissionResourceType.MICROPHONE)) { if (_micGranted) { return PermissionResponse( resources: resources, action: PermissionResponseAction.GRANT, ); } else { // Request permission again await _requestMicrophonePermission(); return PermissionResponse( resources: resources, action: _micGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY, ); } } return PermissionResponse( resources: resources, action: PermissionResponseAction.DENY, ); } void _showErrorDialog(String message) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Error'), content: Text(message), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); } @override Widget build(BuildContext context) { if (!_micGranted) { return Scaffold( appBar: AppBar(title: const Text('WebView')), body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.mic_off, size: 64, color: Colors.red), SizedBox(height: 16), Text( 'Microphone permission is required for voice features', textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), ], ), ), ); } return Scaffold( appBar: AppBar( title: const Text('WebView'), actions: [ if (_isLoading) const Center( child: Padding( padding: EdgeInsets.all(16.0), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), ], ), body: InAppWebView( initialUrlRequest: URLRequest( url: WebUri(widget.targetUrl), ), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: false, allowsInlineMediaPlayback: true, useHybridComposition: true, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, ), onWebViewCreated: (controller) { _webViewController = controller; }, onPermissionRequest: (controller, request) async { return await _handlePermissionRequest(request.resources); }, onLoadStart: (controller, url) { setState(() { _isLoading = true; }); }, onLoadStop: (controller, url) { setState(() { _isLoading = false; }); }, onLoadError: (controller, url, code, message) { print('WebView load error: $code - $message'); _showErrorDialog('Failed to load webpage: $message'); }, onConsoleMessage: (controller, consoleMessage) { print('Console: ${consoleMessage.message}'); }, ), ); } } ``` ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] Check the following requirements: 1. βœ… App has microphone permissions in device settings 2. βœ… Manifest permissions are declared correctly 3. βœ… Permission handler is implemented properly 4. βœ… WebView permission delegation is configured ```dart // Check permission status Future checkPermissionStatus() async { PermissionStatus status = await Permission.microphone.status; print('Microphone permission status: $status'); if (status.isPermanentlyDenied) { // Open app settings await openAppSettings(); } } ``` Verify connectivity and configuration: 1. βœ… Check internet connectivity 2. βœ… Verify URL accessibility in browser 3. βœ… Ensure JavaScript is enabled 4. βœ… Check mixed content settings ```dart // Add comprehensive error handling onLoadError: (controller, url, code, message) { print('Load error: $code - $message'); print('Failed URL: $url'); // Show user-friendly error ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load webpage')), ); } ``` Debug JavaScript execution issues: 1. βœ… Verify JavaScript is enabled in WebView settings 2. βœ… Check console messages for errors 3. βœ… Test website in mobile browser first 4. βœ… Ensure all required permissions are granted ```dart // Monitor console messages onConsoleMessage: (controller, consoleMessage) { print('Console [${consoleMessage.messageLevel}]: ${consoleMessage.message}'); if (consoleMessage.messageLevel == ConsoleMessageLevel.ERROR) { // Handle JavaScript errors print('JavaScript error detected'); } } ``` ### Best Practices [#best-practices] **Security & Permissions:** * Only grant necessary permissions to WebView * Restrict WebView to trusted domains * Always request user permission before accessing microphone * Handle permission denials gracefully with user-friendly messages **Performance Optimization:** * Use lazy loading for WebView when possible * Cache permission status to avoid repeated requests * Implement proper error handling and loading states * Monitor WebView memory usage and dispose properly **Configuration Management:** * Use environment variables for sensitive URLs * Create configurable settings for different environments * Test with various target websites during development * Implement logging for debugging and monitoring ## Support [#support] For additional help: * **Email Support:** [contact@revrag.ai](mailto:contact@revrag.ai) *** **Last Updated:** June 2025 --- # React Native WebView > React Native WebView Integration Guide - Enable microphone access for Revrag Embed Agent in mobile apps URL: /embed/web-view-integration/react-native Markdown: /embed/web-view-integration/react-native.md {/* React Native WebView Development Light React Native WebView Development Dark */} # React Native WebView Integration Guide [#react-native-webview-integration-guide] ## Overview [#overview] This React Native WebView integration provides a solution for the microphone access challenge faced by Revrag's embed agent when implemented in mobile apps using WebView. The embed agent requires microphone access to enable voice-based AI assistance for website navigation, but traditional WebView implementations often fail to properly delegate microphone permissions to the embedded website. ## Problem Statement [#problem-statement] Revrag's embed agent is a JavaScript SDK that can be integrated into any website to provide AI-powered voice assistance. However, when mobile apps load websites containing the embed agent in a WebView, the microphone access required by the agent is often blocked or not properly delegated, preventing users from interacting with the AI assistant. ## Prerequisites [#prerequisites] * React Native 0.72+ * iOS 13+ / Android API 21+ * Microphone permissions on target device This integration requires proper setup of WebView microphone permissions and real-time audio communication capabilities. ## Installation [#installation] Install the required packages using your preferred package manager: ```bash npm npm install react-native-webview react-native-permissions ``` ```bash yarn yarn add react-native-webview react-native-permissions ``` ```bash pnpm pnpm add react-native-webview react-native-permissions ``` ### iOS Pod Installation [#ios-pod-installation] For iOS, run pod install after installing dependencies: ```bash cd ios && pod install && cd .. ``` ## Solution Overview [#solution-overview] This project implements a React Native WebView solution using the `react-native-webview` package with proper microphone permission handling. The solution: 1. **Requests native microphone permissions** using the `react-native-permissions` package 2. **Delegates permissions to the WebView** when the embed agent requests microphone access 3. **Handles permission states** across both native and WebView contexts 4. **Provides a seamless user experience** for voice interactions ### Key Features [#key-features] * βœ… **Native Microphone Permission Handling**: Properly requests and manages microphone permissions at the app level * βœ… **WebView Permission Delegation**: Seamlessly delegates permissions to the embedded website * βœ… **Cross-Platform Support**: Works on both Android and iOS * βœ… **Error Handling**: Comprehensive error handling and user feedback * βœ… **Configurable**: Easy configuration for different websites and use cases ## Android Configuration [#android-configuration] ### 1. Android Manifest Permissions [#1-android-manifest-permissions] Add the following permissions to your `android/app/src/main/AndroidManifest.xml`: ```xml ``` **Permission Purposes:** * `RECORD_AUDIO`: Grants permission to record audio from microphone * `MODIFY_AUDIO_SETTINGS`: Allows modification of audio settings * `INTERNET`: Required for WebView to load web content * `ACCESS_NETWORK_STATE`: Required for network connectivity checks ### 2. Android MainActivity Configuration [#2-android-mainactivity-configuration] Update your `android/app/src/main/java/com/yourapp/MainActivity.kt`: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val webView = WebView(this) webView.settings.javaScriptEnabled = true webView.settings.mediaPlaybackRequiresUserGesture = false webView.webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest?) { request?.grant(request.resources) // Grant mic/camera } } } ``` **Key Configuration Features:** * **JavaScript Enabled**: Allows web content to execute JavaScript code * **Media Playback**: Disables user gesture requirement for media playback * **Permission Handler**: Automatically grants microphone and camera permissions ## iOS Configuration [#ios-configuration] ### 1. iOS Permissions [#1-ios-permissions] **CRITICAL:** Add the following permissions to your `ios/YourAppName/Info.plist`. Missing `NSMicrophoneUsageDescription` will cause the app to crash when accessing the microphone. ```xml NSMicrophoneUsageDescription This app needs access to microphone to enable voice functionality in the webview. NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsLocalNetworking ``` ## WebView Implementation [#webview-implementation] ### Permission Request Handling [#permission-request-handling] ```jsx import React, { useState, useEffect } from 'react'; import { PermissionsAndroid, Platform } from 'react-native'; import { WebView } from 'react-native-webview'; const MicrophoneWebView = () => { const [micGranted, setMicGranted] = useState(false); useEffect(() => { async function requestMic() { if (Platform.OS === 'android') { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: 'Microphone Permission', message: 'App needs access to your microphone', buttonPositive: 'OK', }, ); setMicGranted(granted === PermissionsAndroid.RESULTS.GRANTED); } else { setMicGranted(true); // iOS will prompt automatically via Info.plist } } requestMic(); }, []); return ( // WebView component here ); }; ``` #### WebView Configuration [#webview-configuration] ```jsx { console.log('Permission request from WebView:', event.resources); // Grant only microphone if (event.resources.includes('android.webkit.resource.AUDIO_CAPTURE')) { event.grant(['android.webkit.resource.AUDIO_CAPTURE']); } else { event.deny(); } }} onError={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; console.error('WebView error: ', nativeEvent); }} /> ``` #### JavaScript Injection for Testing [#javascript-injection-for-testing] ```javascript const injectedJS = ` (function() { console.log("Injected JS: Testing microphone access..."); if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { console.log("Microphone access granted in WebView"); // Stop the stream immediately after testing stream.getTracks().forEach(track => track.stop()); }) .catch(err => { console.error("Microphone access denied in WebView:", err); }); } else { console.error("getUserMedia not supported in this WebView"); } return true; // Required for Android })(); `; ``` ## Platform-Specific Configuration [#platform-specific-configuration] ### Android [#android] The Android configuration includes native WebView settings and permission handling as shown above. ### iOS [#ios] Add the microphone usage description to `ios/YourApp/Info.plist`: ```xml NSMicrophoneUsageDescription This app needs access to microphone to enable voice functionality in the webview. ``` ## Permission Flow [#permission-flow] 1. **App Launch**: Android system checks for declared permissions 2. **Runtime Request**: React Native requests microphone permission from user 3. **WebView Permission**: WebView automatically grants microphone access to web content 4. **Web Content Access**: JavaScript can access microphone via `navigator.mediaDevices.getUserMedia()` ## Testing [#testing] ### 1. Test HTML Files [#1-test-html-files] Create test files in `android/app/src/main/assets/` for local testing: **test-microphone.html**: ```html Microphone Test

Microphone Access Test

``` ### 2. Manual Testing Steps [#2-manual-testing-steps] 1. Launch the app and navigate to the WebView screen 2. Check console logs for permission status 3. Verify microphone access in web content 4. Test audio recording functionality ### 3. Debug Information [#3-debug-information] * Console logs show permission request status * WebView error handling for failed loads * JavaScript console messages are captured ## Integration with Revrag Embed Agent [#integration-with-revrag-embed-agent] To integrate this solution with Revrag's embed agent: 1. **Deploy the embed agent** on your target website 2. **Update the WebView source URL** to point to your website 3. **Test microphone access** by triggering the embed agent's voice functionality The embed agent should now be able to access the microphone and provide voice-based assistance to users. ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] Check the following requirements: 1. βœ… Android manifest permissions declared 2. βœ… Runtime permission requests implemented 3. βœ… WebView permission handling configured 4. βœ… App permissions granted in device settings ```jsx // Check permission status const checkPermission = async () => { const granted = await PermissionsAndroid.check( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO ); console.log('Microphone permission:', granted); }; ``` Verify connectivity and configuration: 1. βœ… Check internet connectivity 2. βœ… Verify URL accessibility in browser 3. βœ… Check WebView configuration settings 4. βœ… Ensure JavaScript is enabled ```jsx // Add error handling { const { nativeEvent } = syntheticEvent; console.error('WebView error:', nativeEvent); }} onHttpError={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; console.error('HTTP error:', nativeEvent.statusCode); }} /> ``` Debug microphone access issues: 1. βœ… Verify device microphone functionality 2. βœ… Check app permissions in device settings 3. βœ… Ensure WebView JavaScript is enabled 4. βœ… Test with microphone test HTML ```jsx // Test microphone access const testMicrophone = ` navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { console.log("Microphone access granted"); stream.getTracks().forEach(track => track.stop()); }) .catch(err => console.error("Microphone error:", err)); `; ``` ### Debug Steps [#debug-steps] 1. Check the console logs for permission status 2. Verify native permissions are granted 3. Test WebView microphone access using test HTML files 4. Ensure the target website supports microphone access ### Best Practices [#best-practices] **Security & Permissions:** * Only grant necessary permissions to WebView * Restrict WebView to trusted domains using origin whitelist * Always request user permission before accessing microphone * Ensure audio data is handled securely and privately **Performance Optimization:** * Load WebView only when needed (lazy loading) * Cache permission status to avoid repeated requests * Implement graceful fallbacks for permission denials * Properly dispose of WebView resources to prevent memory leaks **Configuration Management:** * Use environment variables for sensitive information * Create reusable configuration objects * Test with different target URLs during development * Monitor WebView performance and error rates ## Usage Examples [#usage-examples] ### Complete Implementation Example [#complete-implementation-example] ```jsx import React, { useState, useEffect } from 'react'; import { View, StyleSheet, PermissionsAndroid, Platform, Alert } from 'react-native'; import { WebView } from 'react-native-webview'; const MicrophoneWebView = ({ targetUrl = 'https://your-website.com' }) => { const [micGranted, setMicGranted] = useState(false); const [isLoading, setIsLoading] = useState(true); useEffect(() => { requestMicrophonePermission(); }, []); const requestMicrophonePermission = async () => { try { if (Platform.OS === 'android') { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: 'Microphone Permission', message: 'This app needs access to your microphone for voice features', buttonPositive: 'Allow', buttonNegative: 'Deny', }, ); setMicGranted(granted === PermissionsAndroid.RESULTS.GRANTED); } else { // iOS will prompt automatically via Info.plist setMicGranted(true); } } catch (error) { console.error('Permission request failed:', error); Alert.alert('Error', 'Failed to request microphone permission'); } setIsLoading(false); }; const handlePermissionRequest = (event) => { const { resources } = event; if (resources.includes('android.webkit.resource.AUDIO_CAPTURE')) { event.grant(['android.webkit.resource.AUDIO_CAPTURE']); } else { event.deny(); } }; if (isLoading) { return ( Requesting permissions... ); } if (!micGranted) { return ( Microphone permission is required for voice features ); } return ( { const { nativeEvent } = syntheticEvent; console.error('WebView error:', nativeEvent); }} onLoadStart={() => setIsLoading(true)} onLoadEnd={() => setIsLoading(false)} /> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, }); export default MicrophoneWebView; ``` ## Support [#support] For additional help: * **Email Support:** [contact@revrag.ai](mailto:contact@revrag.ai) *** **Last Updated:** June 2025 --- # Webhooks > Receive a server-to-server notification with the full call result when an in-app agent call ends. URL: /embed/webhooks Markdown: /embed/webhooks.md ## Overview [#overview] When an in-app agent call finishes, RevRag can send a **`call.ended`** webhook β€” a `POST` request to an HTTPS endpoint on your server β€” containing the complete result of that call: status, duration, summary, transcript, recording URL, and your `app_user_id`. The [payload](#payload) below shows exactly what your server receives when an in-app call ends. Webhooks are **server-to-server**. They are delivered to your backend, not to the SDK running in your app. The in-app SDK's own [agent lifecycle events](#in-app-events-vs-webhooks) (e.g. `agent_start` / `agent_end`) are a separate, in-process mechanism β€” they are not the same as this webhook. ## Enabling webhooks [#enabling-webhooks] Webhooks are **opt-in** and configured per agent. They are not enabled by default. To turn them on for your in-app agent, contact **[contact@revrag.ai](mailto:contact@revrag.ai)** with: * **Webhook URL** β€” the HTTPS endpoint that will receive the `POST` (HTTP is not supported). * **Signing secret** β€” a shared secret used to verify each request (see [Security & headers](#security--headers)). ## Security & headers [#security--headers] In-app webhooks use the **same** signing scheme as every RevRag webhook β€” HMAC-SHA256 computed over `"{timestamp}.{raw_body}"` with your shared secret. Each request carries: | Header | Purpose | | --------------------- | --------------------------------------------- | | `X-Webhook-Event` | Event type (`call.ended`) | | `X-Webhook-Timestamp` | Unix timestamp (seconds) the webhook was sent | | `X-Webhook-Signature` | `t=,v1=` | | `X-Webhook-ID` | Unique id for deduplication | Always verify the signature before trusting a payload. See [Webhook Security](/api-reference/webhook-security) for the full verification steps and ready-to-use Python / Node.js examples. ## Payload [#payload] The webhook body is the same JSON shape as the [Get Call Status](/api-reference/campaigns-api#get-call-status) response. For an in-app call it carries your **`app_user_id`** as a top-level correlation field. ```json { "call_status": "ENDED", "call_id": "123e4567-e89b-12d3-a456-426614174000", "agent_id": "33333333-3333-3333-3333-333333333333", "summary": "User asked about their loan eligibility and was guided to the application.", "start_time": "2026-05-27T03:05:00+00:00", "end_time": "2026-05-27T03:06:12+00:00", "duration": 72, "disconnection_reason": "user_hangup", "recording_url": "https://.../recording.mp3", "transcription": { "messages": [ { "role": "assistant", "content": "Hi! How can I help you today?" }, { "role": "user", "content": "What documents do I need for a loan?" } ] }, "custom_variables": [ { "key": "interested_in_loan", "type": "boolean", "value": true } ], "variables_fields": { "plan": "gold" }, "app_user_id": "user_12345" } ``` ### Payload parameters [#payload-parameters] | Field | Type | Description | | ------------------------- | -------------- | ----------------------------------------------------------------------------- | | `call_status` | string | Final call status β€” `ENDED` once the call has finished | | `call_id` | string (uuid) | The call this notification is for | | `agent_id` | string (uuid) | The in-app agent that handled the call | | `summary` | string | LLM-generated call summary (when a transcript exists) | | `start_time` / `end_time` | string \| null | ISO-8601 timestamps | | `duration` | number | Call duration in seconds | | `disconnection_reason` | string \| null | Normalized hangup reason | | `recording_url` | string \| null | Presigned recording URL (for transcribed calls) | | `transcription` | object \| null | Turn-by-turn `messages` (`role`, `content`) | | `custom_variables` | array | Post-call variables extracted by the agent (`key`, `type`, `value`) | | `variables_fields` | object \| null | Variables associated with the call | | `app_user_id` | string \| null | Your user id, echoed back top-level β€” see [Correlation IDs](#correlation-ids) | ## Retries & failure recovery [#retries--failure-recovery] If your endpoint is temporarily unreachable, RevRag retries automatically with exponential backoff. If every attempt fails, the delivery is retained as a **dead-letter** so you can recover it later via the polling API below. ### Retry policy [#retry-policy] Each webhook goes through up to **3 attempts** in total: | Attempt | Timing | | ------- | ------------------------------------------------------------ | | 1 | Fires immediately after the call ends | | 2 | Retries **5 minutes** after attempt 1 fails | | 3 | Retries **15 minutes** after attempt 2 fails | | β€” | If attempt 3 fails, the delivery is moved to **dead-letter** | Any non-`2xx` response β€” including network errors, timeouts, and 4xx β€” is treated as a failure and triggers a retry. Every retry carries the **same `X-Webhook-ID`** as the original attempt. Dedupe on this header so a delivered-but-ack-lost webhook doesn't get processed twice. ### Discovering failed deliveries [#discovering-failed-deliveries] Poll this endpoint at a regular cadence (for example, every 5 minutes) to discover any calls whose webhook could not be delivered. Only dead-letter deliveries are returned β€” successful and in-progress deliveries are not included. **GET** `/webhook-failures` #### Header Parameters [#header-parameters] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `X-API-Key` | string | Yes | API Key for authentication | #### Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `since` | string (ISO 8601) | Yes | Start of the window (exclusive). On subsequent polls, pass your previous request's `until` value here. | | `until` | string (ISO 8601) | No | End of the window (inclusive). Defaults to the current time. | | `limit` | integer | No | Rows per page. Default `100`, maximum `500`. | | `cursor` | string | No | Opaque pagination cursor. Pass unchanged from the previous response's `next_cursor` to fetch the next page. | #### Response [#response] ```json { "items": [ { "call_id": "123e4567-e89b-12d3-a456-426614174000", "failed_at": "2026-05-27T03:20:00.000Z", "last_status_code": 500, "attempts": 3 } ], "next_cursor": null } ``` | Field | Type | Description | | -------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `items[].call_id` | string (uuid) | Identifier of the call whose webhook failed. Use this to fetch the full payload (see below). | | `items[].failed_at` | string (ISO 8601) | When the delivery entered the failed state, after all retries were exhausted. | | `items[].last_status_code` | integer \| null | HTTP status returned by your endpoint on the final attempt. `null` if every attempt failed at the network layer (timeout, connection refused, DNS failure). | | `items[].attempts` | integer | Total delivery attempts made before giving up. | | `next_cursor` | string \| null | Pass this value as `cursor` on the next request to fetch more results within the same window. `null` when the window is fully drained. | #### Example [#example] ```bash curl -X GET "https://api.revrag.ai/webhook-failures?since=2026-05-27T00:00:00Z&limit=100" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Fetching the missed payload [#fetching-the-missed-payload] For each `call_id` returned above, fetch the full call payload β€” the same JSON shape as the webhook body β€” from the [Get Call Status](/api-reference/call-trigger-apis#3-get-call-status) endpoint: ```bash curl -X GET "https://api.revrag.ai/v1/campaigns/trigger/status/{call_id}" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Recommended polling loop [#recommended-polling-loop] 1. Persist the timestamp of your last successful poll (`last_poll_at`). 2. Every few minutes, call `GET /webhook-failures?since=&until=`. 3. For each `call_id` in the response, call `GET /v1/campaigns/trigger/status/{call_id}` and process the payload the same way your webhook handler would. 4. If `next_cursor` is present, paginate through by re-calling with `&cursor=` until `next_cursor` is `null`. 5. Set `last_poll_at = ` for the next poll. This guarantees no call is lost even if your endpoint is briefly unreachable. ## Correlation IDs [#correlation-ids] To tie a webhook back to the right user in your own system, use **`app_user_id`** together with **`call_id`**. The `app_user_id` you pass when requesting an in-app call token is echoed back top-level in every webhook, so you can attribute the call to the exact user who initiated it. ## In-app events vs webhooks [#in-app-events-vs-webhooks] Don't confuse these two mechanisms: | | In-app SDK events | Webhooks (this page) | | ----------- | ----------------------------------------------------- | ---------------------------------------------------------- | | **Where** | In your app process (browser / mobile) | Server-to-server, to your backend | | **When** | Live, during the call (`agent_start`, `agent_end`, …) | After the call ends (`call.ended`) | | **Use for** | Driving UI, real-time state | Persisting the call result, transcript, summary, analytics | For the in-process events, see your platform's integration guide. ## Related [#related] Full HMAC-SHA256 verification steps with Python and Node.js examples. The same payload contract as documented for campaign calls. --- # Introduction > AI Sales & Onboarding agents for higher conversions in BFSI, Fintech & Insurtech URL: /general/introduction Markdown: /general/introduction.md AI Sales Technology Light AI Sales Technology Dark ## About RevRag.ai [#about-revragai] RevRag.ai empowers revenue teams with **AI Sales & Onboarding agents** designed specifically for BFSI, Fintech, and Insurtech companies. Our human-like AI agents handle multi-channel engagement to boost conversions and streamline customer onboarding. ## Meet Our AI Agents [#meet-our-ai-agents] ### AI Sales Agent [#ai-sales-agent] Our AI Sales Agent qualifies high-intent leads and handles pre-sales queries at scale for lending, insurance, and financial services. It drives conversations toward conversion events like disbursement, issuance, and appointments. **Key Capabilities:** * Lead qualification through AI calling * Pre-sales query resolution * Conversion-focused conversations * Cross-sell and upsell opportunities ### AI Onboarding Agent [#ai-onboarding-agent] Our AI Onboarding Agent improves customer activation by providing proactive assistance during onboarding. It re-engages dropped users through AI calling and offers 24/7 live onboarding support. **Key Capabilities:** * Proactive user assistance via AI voice * Re-engagement of dropped users * Real-time navigation assistance * Seamless onboarding experience * Activation and re-activation of users ## Integration Options [#integration-options] Integrate RevRag.ai's AI capabilities directly into your mobile and web applications ## Why Choose RevRag.ai? [#why-choose-revragai] Built specifically for BFSI, Fintech, and Insurtech with deep domain understanding Clients achieve 3X faster document collection and improved conversion rates Advanced AI that provides natural, contextual conversations across all channels Enterprise-grade robust security and compliance for financial services ## Backed by Industry Leaders [#backed-by-industry-leaders] RevRag.ai is backed by prominent investors including Powerhouse Ventures, founders of 6Sense, Slintel, and other marquee investors who believe in our vision to revolutionize revenue teams with AI. Ready to transform your revenue operations? Our AI agents are designed to scale smarter, faster, and better. --- # Telephony Integration Without SIP Trunking > How telephony partners that cannot provide a SIP trunk can connect their phone system to a RevRag AI voice agent β€” options, flowcharts, and trade-offs. URL: /integrations/telephony-without-sip Markdown: /integrations/telephony-without-sip.md **Audience:** Telephony partners, BPOs, and contact-centre operators who want to connect their phone system to a RevRag AI voice agent but **cannot provide a standard SIP trunk** (no SIP trunk URL + DID + credentials to hand over). **Status:** Reusable reference, shared by direct link only. Applicable to any telephony partner without SIP connectivity. *** ## 1. Purpose [#1-purpose] RevRag's normal integration is a **SIP trunk**: the partner shares a trunk URL, a DID/phone number, and a username/password, and RevRag's voice pipeline connects to it. Some partners run **GSM-based phone systems** (SIM banks, mobile dialers, handset-based telecalling) and **cannot expose a SIP trunk**. This document lists every viable way to connect such a system to a RevRag voice agent, with flowcharts, action items for each side, and honest pros/cons β€” so a partner can pick the option that matches what their system *can* do. *** ## 2. The one constraint that shapes everything [#2-the-one-constraint-that-shapes-everything] > **A cloud voice agent needs a real-time IP (VoIP) audio path. A GSM/PSTN call is not IP. So *somewhere*, the call's audio must be converted GSM/PSTN β†’ IP.** There are only three places that conversion can happen: | Where GSMβ†’IP happens | What it requires | SIP on partner side? | | ------------------------------------------------------------------------ | --------------------------------------------------------------------- | -------------------- | | **At the partner's premises** | A GSM gateway / SBC (hardware) | Yes β€” produces SIP | | **Inside the public phone network**, on a number **RevRag** already owns | Nothing new from the partner β€” just a normal phone call to our number | **No** | | **At the partner's IP-PBX/dialer** that already speaks SIP | Their existing SIP capability | Yes | The middle row is the key to a no-SIP integration: **route the live call over the normal phone network to a RevRag-operated number**, and let RevRag's existing carrier do the GSMβ†’IP conversion. The partner never touches SIP. **Impossible combination (state this up front):** *"Calls must originate/stay on our own SIMs"* **+** *"single, clean audio hop"* **+** *"no SIP and no new hardware"* cannot all be true at once. If single-hop audio on the partner's own SIMs is mandatory, a gateway/SIP path (Section 7) is unavoidable. *** ## 3. Quick chooser [#3-quick-chooser] ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Does your phone system speak SIP at all, β”‚ β”‚ or can you add a GSM gateway / SBC? β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ YES β”‚ NO β–Ό β–Ό Use the standard SIP β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” integration (Section 7 β”‚ Which direction of calls? β”‚ / separate SIP guide). β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ INBOUND β”‚ OUTBOUND β–Ό β–Ό Solution A Solution B (preferred) (call forward) or Solution C ``` | If you need… | Use | | --------------------------------------------------------- | ------------------------------------------------------------ | | Customers call **your** number, bot answers | **Solution A** β€” Call forwarding to a RevRag DID | | Bot calls customers, **dialed from your SIMs / your CLI** | **Solution B** β€” Outbound dial + bridge to bot (recommended) | | Bot calls customers, you only manage the **contact list** | **Solution C** β€” RevRag dials, you integrate via API | | Best possible audio quality, willing to add hardware | **Solution D** β€” GSM gateway / SBC (true SIP) | | Move the number permanently to RevRag | **Solution E** β€” Number porting / handover | *** ## 4. Solution A β€” Inbound via call forwarding *(no SIP, no new hardware)* [#4-solution-a--inbound-via-call-forwarding-no-sip-no-new-hardware] **Use when:** customers dial a number the partner publishes, and the bot should answer. **Idea:** The partner sets **call forwarding** (operator/SIM feature) on their published number to a **RevRag-operated DID**. The bot answers on RevRag's existing inbound trunk. GSMβ†’IP happens at RevRag's carrier, invisible to the partner. ### Flowchart [#flowchart] ``` Customer mobile β”‚ dials partner's published number β–Ό Partner number / SIM β”‚ GSM/operator CALL FORWARD (always / on-busy / on-no-answer) β–Ό RevRag DID ──(RevRag's existing carrier SIP trunk)──▢ Voice Agent β”‚ bot answers & talks ``` ### Action items [#action-items] | RevRag | Partner | | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Allocate a RevRag DID, bind it to the right agent/workspace (existing inbound trunk + dispatch rule). | Set **call forwarding** on the customer-facing number(s) to the RevRag DID. | | Decide caller-ID handling (see cons); optionally use a **dedicated DID per campaign** to encode context. | Choose forwarding type (always / busy / no-answer). | | (Optional) Configure **bot β†’ human handback** (bot places an outbound call to the partner's queue). | Confirm with their operator whether the **original caller's number is preserved** on forward. | ### Pros [#pros] * Zero SIP and zero new hardware for the partner β€” pure operator feature. * Fastest to pilot; reuses RevRag's existing inbound telephony (no new vendor). * Single GSM/PSTN hop β†’ audio quality close to RevRag's normal inbound calls. ### Cons [#cons] * **Caller ID is operator-dependent** β€” the bot may see the partner's number instead of the customer's. Mitigate with per-campaign DIDs or out-of-band context. * The forwarded leg is **billed to the partner** (operator charge for redirected minutes). * Inbound only. The partner's system steps out of the live audio path. *** ## 5. Solution B β€” Outbound via dial + bridge *(no SIP, no new hardware)* β€” **recommended for outbound** [#5-solution-b--outbound-via-dial--bridge-no-sip-no-new-hardware--recommended-for-outbound] **Use when:** the bot must call customers but the calls should be **dialed from the partner's own SIMs / CLI** (the partner keeps their dialing workflow, identity, and compliance). **Idea:** The partner dials the customer outbound as usual; on answer, the partner places a **second call to a RevRag DID** (a normal phone call β†’ reaches the bot), then **bridges/conferences** the two legs. This is the standard dialer "agent-bridge" pattern, with the bot in the agent's seat. ### Flowchart [#flowchart-1] ``` Leg A: Partner ──GSM outbound──▢ Customer (customer answers) Leg B: Partner ──phone call──▢ RevRag DID ──carrier SIP──▢ Voice Agent (answers, waits) β”‚ β–Ό Bridge / 3-way conference ( Leg A + Leg B ) β”‚ β–Ό Customer ⇄ Voice Agent talk ``` ### Recommended sequence [#recommended-sequence] 1. Partner dials the **customer** β†’ customer answers. 2. Partner dials the **RevRag DID** β†’ bot answers and **waits silently**. 3. Partner **bridges** the two legs. 4. Bot speaks its opener on a **trigger** (first customer audio / DTMF marker / short delay) β€” **not** on call-answer, so the greeting isn't clipped. ### Action items [#action-items-1] | RevRag | Partner | | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Provide a dedicated inbound DID β†’ correct agent/workspace. | Dial the **customer** via existing SIMs. | | Gate the bot's **opener on a bridge trigger**, not on answer. | On answer, dial the **RevRag DID** as a second leg. | | Decide context-passing: dedicated DID per campaign, **DTMF** digits, or an **API** call at bridge time. | **Bridge/conference** the two legs (PBX conference or handset 3-way merge). | | Size inbound DID **concurrency** for the campaign. | **Propagate hangup both ways** (either side drops β†’ tear down both legs). | | Return transcript / recording / disposition via webhook/API. | Ensure the bridge has **echo cancellation** enabled. | ### Pros [#pros-1] * No SIP, no new hardware; the partner keeps **their SIMs, their CLI, their dialing workflow**. * Bot is reached as an ordinary phone call β€” identical to inbound on RevRag's side. * Works with any system that can place two calls and conference them (even a handset's 3-way merge). ### Cons (sharper for a *bot* than for a human agent) [#cons-sharper-for-a-bot-than-for-a-human-agent] * **Two GSM/PSTN hops + double transcoding** (customer↔bridge↔RevRag) β†’ measurably worse audio into speech-to-text. This is the biggest risk β€” **pilot and listen first**. * **Added latency** from the extra leg + bridge β†’ affects turn-taking and interruption handling. * **Echo**: relies entirely on the partner's bridge doing echo cancellation; without it the bot may hear itself. * **Customer identity**: the bot leg's caller ID is the partner's number, not the customer's β†’ use a dedicated DID, DTMF context digits, or an API hand-off. * **2 legs per conversation** (customer + RevRag DID), both billed to the partner; DTMF keypad capture across two bridged GSM legs can be flaky. *** ## 6. Solution C β€” Outbound via RevRag dialer + API integration *(no SIP, no new hardware)* [#6-solution-c--outbound-via-revrag-dialer--api-integration-no-sip-no-new-hardware] **Use when:** the partner is fine with **RevRag placing the calls** and only wants to manage the contact list and receive results. **Idea:** The bot dials customers through **RevRag's own existing outbound telephony** (the campaign engine). The partner integrates only at the **data/API layer**. ### Flowchart [#flowchart-2] ``` Partner system β”‚ API: contact list / trigger β–Ό RevRag campaign engine ──RevRag's existing outbound trunk──▢ Customer mobile ⇄ Voice Agent β”‚ β”‚ webhook/API: recording, transcript, disposition, post-call variables β–Ό Partner system (CRM / workflow) ``` ### Action items [#action-items-2] | RevRag | Partner | | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Expose/reuse the campaign / outbound-call **API** + a **results webhook**. | Push **contact lists / triggers** via the API; consume results. | | Place calls via RevRag's outbound trunk; present an agreed **CLI** (subject to regulation). | Provide the **CLI** they want presented and confirm authorization to use it. | ### Pros [#pros-2] * No telephony work for the partner at all β€” just an API integration. * **Single, clean audio hop** β†’ best outbound audio quality of the no-hardware options. ### Cons [#cons-1] * Calls are **not dialed from the partner's SIMs** β€” their phone system is out of the call path entirely. * Presenting the partner's number as CLI from RevRag's trunk is **regulated** (e.g., TRAI in India) and depends on RevRag's carrier supporting it. *** ## 7. Solution D β€” GSM gateway / SBC at partner premises *(reintroduces SIP β€” best quality)* [#7-solution-d--gsm-gateway--sbc-at-partner-premises-reintroduces-sip--best-quality] **Use when:** the partner can add hardware and wants the **best audio quality** and/or must keep calls on **their own SIMs in a single bot-controlled leg**. **Idea:** A **GSM gateway / SIM bank** (e.g., Dinstar, Yeastar TG/TA, Portech, Synway) or an SBC converts GSM↔SIP on the partner's side. That device *is* a SIP endpoint, so the integration becomes the standard SIP flow β€” RevRag provides the trunk and the gateway connects to it (inbound and/or outbound). ### Flowchart [#flowchart-3] ``` INBOUND: Customer ─GSM─▢ Gateway (SIM) ─GSMβ†’SIP─▢ RevRag SIP ─▢ Voice Agent OUTBOUND: RevRag SIP ─SIP─▢ Gateway ─SIPβ†’GSM─▢ SIM dials ─▢ Customer ``` ### Pros [#pros-3] * **Single audio hop, best quality and latency** β€” ideal for speech-to-text. * Keeps the partner's **own SIMs / CLI** in a fully bot-controlled call (both directions). * Standard, well-understood SIP integration. ### Cons [#cons-2] * Requires **new hardware** the partner owns/maintains (and SIM channel capacity = concurrency ceiling). * Some setup effort (firewall, allowlisting, codec/DTMF config). > If this path is viable, follow RevRag's **standard SIP integration guide** instead of this document. *** ## 8. Solution E β€” Number porting / handover *(structural)* [#8-solution-e--number-porting--handover-structural] **Use when:** the partner is willing to move the customer-facing number to RevRag permanently. **Idea:** Port the number to RevRag's telephony so customer calls land directly on the bot. Heaviest option; regulatory and operationally involved. Usually only worth it for a long-term, single-purpose number. * **Pros:** cleanest long-term inbound path; single hop; no per-call partner action. * **Cons:** porting is slow and regulated; the partner loses direct control of the number. *** ## 9. Solution comparison at a glance [#9-solution-comparison-at-a-glance] | | A: Forward | B: Dial+Bridge | C: RevRag dials | D: Gateway/SIP | E: Porting | | ----------------------- | ---------- | -------------- | --------------- | ----------------------- | ---------- | | Direction | Inbound | Outbound | Outbound | Both | Inbound | | SIP on partner side | No | No | No | **Yes** | No | | New hardware | No | No | No | **Yes** | No | | New vendor | No | No | No | No | No | | Partner's SIMs/CLI used | n/a | **Yes** | No | **Yes** | No | | Audio hops | 1 | **2** | 1 | **1** | 1 | | Expected audio quality | Good | **Fair** | Good | **Best** | Good | | Setup effort | Low | Low–Med | Low | Med | High | | Per-customer variables | Via key\* | Via key\* | **Native** | Native (out) / key (in) | Via key\* | \* *Key = a per-call identifier RevRag can use to attach the right customer's variables: API pre-registration, a DTMF reference code, or reliable caller-ID lookup. See Section 10.* *** ## 10. Custom variables (agent personalization data) β€” where supported [#10-custom-variables-agent-personalization-data--where-supported] RevRag agents are driven by **custom variables**: values injected into the agent's prompt/context to personalize a call (e.g., customer name, loan amount, due date, account status, campaign offer). These come in two kinds, and the integration option decides which kind is possible: * **Agent-level / static variables** β€” the same for every call of that agent (company name, product details, script constants). **Supported in every solution**, because they don't depend on knowing who is on the call. * **Per-call / per-customer dynamic variables** β€” different for each contact (name, amount, reference no.). **Supported only when RevRag can tie the call to a specific customer record at call time.** ### The rule [#the-rule] > Per-customer variables work only when RevRag either **(a) places the call itself** with the variables attached, or **(b) receives a reliable key at call time** to look them up. When RevRag *initiates* a call (its own outbound), it already holds the contact and its variables β€” full personalization, no extra step. When the call *arrives at a RevRag number* (any inbound/bridged leg), RevRag does not inherently know which customer it is, so a key must be supplied. ### Ways to supply per-customer variables to an *inbound / bridged* bot leg [#ways-to-supply-per-customer-variables-to-an-inbound--bridged-bot-leg] | Mechanism | How it works | Granularity | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | **RevRag-initiated outbound** | Variables passed in the call/campaign API payload when RevRag dials | Per customer (native, best) | | **API pre-registration** | Partner calls a RevRag API just before the call/bridge β€” "expect a call for reference X with these variables" β€” and RevRag binds them to the incoming leg | Per customer (recommended for Solution B) | | **DTMF reference code** | Partner sends a customer/reference ID as DTMF on connect; RevRag resolves a pre-uploaded variable map | Per customer (DTMF-reliability caveat) | | **Caller-ID (CLI) lookup** | RevRag matches the customer's phone number to a pre-loaded map | Per customer β€” needs **reliable** caller ID (SIP/gateway: yes; operator call-forward: often masked) | | **Dedicated DID per segment** | The DID itself encodes context | Campaign/segment level only β€” **not** per customer | ### Support by solution [#support-by-solution] | Solution | Agent-level variables | Per-customer variables | | -------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **A β€” Call forwarding (inbound)** | βœ… Yes | ⚠️ Only via API pre-registration / DTMF / dedicated DID. CLI lookup is unreliable (forwarding usually masks the caller's number). | | **B β€” Dial + bridge (outbound)** | βœ… Yes | ⚠️ Yes **if** the partner passes a key β€” **API pre-registration** (recommended) or **DTMF** at bridge time. Without a key, agent-level only. | | **C β€” RevRag dialer + API (outbound)** | βœ… Yes | βœ… **Native** β€” variables are sent per contact in the API payload. | | **D β€” Gateway / SBC (SIP)** | βœ… Yes | Outbound: βœ… native. Inbound: βœ… via **reliable CLI lookup** (SIP carries caller ID), API, or DTMF. | | **E β€” Number porting (inbound)** | βœ… Yes | ⚠️ Via CLI lookup (reliable on a ported number), API, or DTMF. | **Takeaway:** If full per-customer personalization is essential, the strongest options are **C** (RevRag dials with variables) and **D-outbound**. For the no-SIP outbound bridge (**Solution B**), plan an **API pre-registration step** so the bot receives the customer's variables before speaking β€” otherwise the bot runs on agent-level data only. *** ## 11. Cross-cutting technical requirements [#11-cross-cutting-technical-requirements] Applies to whichever solution is chosen (and especially to D's SIP path): * **Codec:** G.711 Β΅-law (PCMU), 8 kHz, is the safe baseline. * **DTMF:** RFC 2833 (telephone-event) on SIP legs; inband tones across bridged GSM legs (Solution B) β€” test detection. * **Caller ID (CLI):** agree how the customer's number reaches the bot (passthrough, dedicated DID, DTMF, or API). * **Number format:** standardise on **E.164** (`+91XXXXXXXXXX`) end-to-end. * **Session timers (RFC 4028):** on any SIP/gateway path, **disable session timers or set `refresher=uas`** β€” IMS/carrier session-timer refresh has silently cut calls at \~10 minutes. * **Security (Solution D):** SIP digest auth + IP allowlisting; SIP-TLS + SRTP if supported; optional IPsec site-to-site VPN. * **Concurrency:** capped by the partner's SIM/channel count (B, D) and by RevRag DID concurrency (A, B, C). * **Audio-quality pilot:** for any **multi-hop** path (Solution B), run a short audio + latency + echo pilot before committing β€” speech-to-text accuracy is the deciding factor. *** ## 12. Information to exchange before a pilot [#12-information-to-exchange-before-a-pilot] | Item | Provided by | | ------------------------------------------------------------------------------------------------------------------------- | ---------------- | | RevRag DID(s) + target agent/workspace mapping | RevRag | | Custom-variable / context passing method (API pre-registration / DTMF / CLI / dedicated DID) + the per-call variable list | RevRag + Partner | | Campaign/outbound API + results webhook spec (Solution C) | RevRag | | Call direction(s) and chosen solution(s) | Partner | | Forwarding capability + caller-ID behaviour (Solution A) | Partner | | Dialer/PBX conference capability + echo cancellation (Solution B) | Partner | | Who owns the customer-facing numbers | Partner | | Gateway make/model + SIM channels (Solution D) | Partner | | Expected concurrency / call volume | Partner | *** ## 13. Glossary [#13-glossary] * **SIP** β€” Session Initiation Protocol; the signalling standard for VoIP calls. * **SIP trunk** β€” a SIP connection that carries calls between two phone systems. * **DID** β€” Direct Inward Dialing number; a phone number that routes to a specific destination. * **GSM gateway / SIM bank** β€” hardware with SIM slots that converts GSM (mobile) calls to/from SIP. * **SBC** β€” Session Border Controller; secures and normalises SIP traffic. * **Bridge / conference** β€” joining two call legs so both parties hear each other. * **CLI** β€” Calling Line Identification; the caller's phone number shown to the called party. * **DTMF** β€” the tones produced by phone keypad presses. * **PSTN** β€” the public switched telephone network. --- # Compliance and Certification > Information about RevRag.ai compliance standards, certifications, and regulatory adherence URL: /trust-security/compliance-certification Markdown: /trust-security/compliance-certification.md # Compliance and Certification [#compliance-and-certification] ## Our Commitment to Security and Compliance [#our-commitment-to-security-and-compliance] RevRag.ai is committed to maintaining the highest standards of security, privacy, and regulatory compliance. This commitment is reflected in our certifications, compliance frameworks, and security practices. ## Current Certifications [#current-certifications] RevRag.ai maintains the following certifications: * **SOC 2 Type II**: Our systems and processes have been audited against the Trust Services Criteria for security, availability, and confidentiality. This certification validates our ongoing compliance with Trust Services Criteria over time * **ISO 27001**: We maintain certification for our information security management system ## Regulatory Compliance [#regulatory-compliance] Our AI agents and platforms adhere to regulations specific to financial services and data protection: * **Data Protection Laws**: We design our systems with GDPR and other applicable data protection principles in mind * **Industry Standards**: We implement security best practices based on NIST Cybersecurity Framework and OWASP security guidelines ## Regular Security Assessments [#regular-security-assessments] RevRag.ai undergoes rigorous assessments to maintain our certifications and compliance status: * **Bi-Annual VAPT**: We conduct Vulnerability Assessment and Penetration Testing (VAPT) of our SDKs, APIs, and platform every 6 months * Independent third-party security audits * Data protection impact assessments ## Security Practices [#security-practices] Our security practices that support our compliance efforts include: * End-to-end encryption for all data in transit and at rest * Multi-factor authentication for system access * Regular security training for all employees * Comprehensive incident response procedures ## Transparency and Documentation [#transparency-and-documentation] We maintain detailed documentation of our compliance efforts: * Security and privacy policies * Data processing records * Risk assessments * Audit trails ## Verification [#verification] Current and prospective customers can request verification of our compliance status by contacting [contact@revrag.ai](mailto:contact@revrag.ai). We can provide certification documentation under NDA. --- # Data Sharing with Third-Party > Information about how RevRag.ai handles data sharing with third-party vendors URL: /trust-security/data-sharing Markdown: /trust-security/data-sharing.md ## **Introduction** [#introduction] This document forms part of the Data Localization & Security Assessment Report (SAR) for our voice agent platform.\ It provides a clear overview of all third-party vendors engaged in processing, transmitting, or storing customer data, along with official statements on their data retention practices. Our objective is to: 1. Identify all relevant third-party providers involved in Text-to-Speech (TTS), Speech-to-Text (STT), Large Language Models (LLM), Telephony, and Cloud Hosting. 2. Demonstrate compliance with data localization, security, and privacy regulations through documented evidence of vendor commitments. 3. Establish internal controls ensuring zero or minimal retention wherever feasible. This appendix supports our claims with: * Vendor Inventory Table – mapping vendors, data types shared, purposes, transfer methods, and compliance measures. * Data Retention Evidence Appendix – direct quotes and source links from vendor documentation confirming their retention and usage policies. *** ## **1. Third-Party Vendor Inventory** [#1-third-party-vendor-inventory] The table below lists the categories of third-party service providers we use, along with details on the nature of the data shared and the safeguards in place. This inventory is reviewed annually and updated whenever a new vendor is onboarded or an existing vendor’s scope changes. | Vendor Category | Vendor Name(s) | Data Shared | Purpose of Sharing | Data Transfer Method | | ------------------------------ | ---------------- | ---------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------ | | Text-to-Speech (TTS) | Multiple vendors | Text content (no PII where avoidable) | Convert text responses to natural speech | Encrypted API calls (HTTPS/TLS 1.2+) | | Speech-to-Text (STT) | Multiple vendors | Audio recordings of user speech (may contain PII) | Convert speech to text for LLM processing | Encrypted API calls | | Large Language Model (LLM) | Multiple vendors | Transcribed text (minimized PII) | Generate AI-based responses | Encrypted API calls | | Telephony Provider | Multiple vendors | Caller phone number, call audio | Enable inbound/outbound calls | Secure SIP/TLS & SRTP | | Cloud Infrastructure / Hosting | AWS Cloud | Audio files, transcripts, application logs, metadata | Secure storage, compute hosting, backup | Encrypted in transit (TLS 1.2+), encrypted at rest (AES-256) | *** ## **2. Third-Party Data Retention Evidence Appendix** [#2-third-party-data-retention-evidence-appendix] For each vendor category, we have gathered official statements from vendor documentation regarding their data retention and usage practices. These references allow auditors to independently verify compliance claims. | Vendor | Official Claim | Exact Quote | Source Link | Config/Usage Notes | | --------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Sarvam (TTS, STT) | Retention is purpose-bound; no 'no storage' guarantee. | "We take reasonable steps to ensure that User data is available only for so long as is necessary for the purpose for which it is processed..." | [Link](https://sarvam.ai/privacy-policy) | Avoid PII in text/audio; scrub logs post-processing. | | Gemini (Google) – LLM | Zero Data Retention achievable by disabling caching in Vertex AI. | "Data sent to Gemini models may be cached up to 24 hours unless caching is disabled." | [Link](https://cloud.google.com/vertex-ai/docs/generative-ai/configure-data-governance) | Disable caching; ensure no abuse-logging exceptions. | | OpenAI – LLM | Zero Data Retention (ZDR) available; default retention up to 30 days for abuse monitoring. | "May securely retain API inputs and outputs for up to 30 days... You can also request zero data retention (ZDR)." | [Link](https://openai.com/enterprise-privacy) | Enable ZDR for eligible endpoints. | | Deepgram – STT | Data from opted-out requests is retained only for request processing. | "Set mip\_opt\_out=true to ensure data is retained only for the duration necessary to process the request." | [Link](https://developers.deepgram.com/docs/model-improvement) | Always set mip\_opt\_out=true for zero retention. | | Azure – LLM, STT, TTS | STT and TTS (prebuilt voices) do not store customer data; LLM not used for training. | "For real-time speech to text, audio input is processed only in server memory, and no data is stored at rest. Neither input text nor output audio content will be stored in Microsoft logs." | [Link](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/data-and-privacy) | Use real-time STT and prebuilt-voice TTS modes for no storage; choose region for storage compliance. | | ElevenLabs – TTS | Zero Retention Mode deletes data immediately after request completion. | "In this Zero Retention Mode, most data in requests and responses are immediately deleted once the request is completed." | [Link](https://elevenlabs.io/docs/resources/zero-retention-mode) | Enable Zero Retention Mode in API requests. | *** ## **3. Implementation & Internal Controls** [#3-implementation--internal-controls] * Data Minimization: We configure each integration to share only the minimum necessary data. Where possible, we strip or anonymize PII before sending it to vendors. * Encryption: All API calls are encrypted in transit (TLS 1.2+), and sensitive data is encrypted at rest. * Zero Retention Settings: Vendors that support zero retention (e.g., OpenAI ZDR, Gemini no-caching, ElevenLabs Zero Retention Mode, Deepgram mip\_opt\_out) are configured accordingly. * Periodic Review: Vendor policies are reviewed quarterly to ensure ongoing compliance with local regulations and customer contractual obligations. *** ## **4. Conclusion** [#4-conclusion] This appendix demonstrates that: * All third-party vendors with access to customer data have been identified. * We have gathered and documented evidence of their retention policies. * Where possible, we actively configure services for zero or minimal data retention. This approach provides transparency, satisfies regulatory requirements, and ensures our voice agent platform adheres to best practices for data localization and security. *** ## Questions and Concerns [#questions-and-concerns] If you have questions about our data sharing practices or wish to exercise your data rights, please contact our Data Protection Officer at [contact@revrag.ai](mailto:contact@revrag.ai).