# Creating a Knowledge Base Source: https://docs.gnani.ai/A01_KB Upload documents or import web content so your agent can answer from trusted sources. A knowledge base holds the information your agent draws on during conversations — FAQs, policies, product details, and support articles. Well-structured content here directly improves answer quality. ## Upload a document Use this when you have files ready on disk. 1. Go to **Knowledge Base → Add Knowledge Base**. 2. In **Knowledge Base Files**, select or drag in PDF, DOC, DOCX, XLS, XLSX, CSV, or TXT files. 3. Click **Next**, name the knowledge base, then **Start Training**. 4. When training finishes, the content is available to any agent you link. **Tips** * Use clear headings and sections in source documents. * To refresh content later, open the knowledge base and upload new files or remove outdated ones. ## Import from a URL Use this for public help centers, product pages, or documentation sites. 1. Go to **Knowledge Base → Add Knowledge Base**. 2. Click **Add a URL**, paste the page URL, and click **Next**. 3. The platform extracts text and follows links one level deep. 4. Select the sub-pages to include, name the knowledge base, and click **Start Training**. **Tips** * The URL must be publicly reachable (no login wall). * If a page mixes marketing copy with support content, a focused PDF upload is often more accurate. ## Verify your content After training, click **Test Knowledge** to preview answers against the uploaded material. Adjust documents or URLs until responses match what you expect. **Next:** [Creating Your First Agent](/A02_Agent) # Creating Your First Agent Source: https://docs.gnani.ai/A02_Agent Create an agent from scratch or a template, then configure prompt, voice, and model settings. An agent defines how your product sounds on a call: the role it plays, the knowledge it uses, and the models that power speech and reasoning. ## Create from scratch Choose this when you want full control over the prompt and settings. 1. Go to **Manage Agents → Create Agent → Create from Scratch**. 2. Enter a name and short description. 3. Click **Create**. Create agent from scratch ## Create from a template Choose this when you want a pre-written system prompt as a starting point. 1. Go to **Manage Agents → Create Agent**. 2. Select a template and click **Proceed**. 3. Name the agent, link a knowledge base, and click **Create**. Create agent from template ## Write the system prompt The system prompt sets role, goals, and tone. Strong prompts usually include: * A clear objective (what the agent should accomplish on each call) * Context about the audience and business * Expected tone and formality * One or two example exchanges when helpful ## Configure messages and models On the agent setup screen: | Setting | Purpose | | ------------------ | ------------------------------------------------------ | | Greeting message | Opening line when the call connects | | Ending message | Closing line before hang-up | | Provider and model | LLM that generates replies | | Knowledge base | Linked documents and URLs | | Temperature | Lower = more literal; higher = more varied wording | | Max tokens | Upper bound on user input the model processes per turn | ## Customize agent details Open the **Customize** tab to set language, region, time zone, and how the agent should describe itself to callers. | Field | Purpose | | -------------------- | --------------------------------------------------------- | | Language | One primary language, or multiple for multilingual agents | | Region and time zone | Used for scheduling and integrations | | Description | Short summary of how the agent should present itself | **Next:** [Testing Your Agent](/A03_Testing) # Testing Your Agent Source: https://docs.gnani.ai/A03_Testing Validate agent responses in chat, in the browser with voice, and on a real phone call. Test before you go live. Each method below catches different issues: text logic, voice latency, and real telephony behavior. ## Test in chat 1. Open your agent and go to **Test → Chat Window → Start Testing**. 2. Send messages and confirm answers match your knowledge base and prompt. Chat testing window ## Test voice in the browser 1. Go to **Test → Web-based (Voice) → Start Testing**. 2. Speak as a caller would and listen to the agent's replies. 3. To share with a teammate, use **Generate Shareable Link** (valid for five minutes, no login required). ## Test on a phone call 1. Whitelist your number under [Whitelisting Numbers](/A04_Whitelisting). 2. Go to **Test → Trigger Agent Call**, select your number, and click **Start Testing**. 3. Answer the incoming call and run through your main scenarios. ## Tuning tips Adjust temperature, system prompt, transcriber, and TTS voice until responses feel natural and stay on policy. Small changes to the prompt often matter more than model switches alone. **Next:** [Whitelisting Numbers](/A04_Whitelisting) # Whitelisting Numbers Source: https://docs.gnani.ai/A04_Whitelisting Simulate a real call ### What Are Whitelisted Numbers? Whitelisted numbers allow you to test your agent’s calling feature. By adding your own phone number, you can receive test calls and interact with your agent in real time, ensuring everything works as expected. ### Why Use Whitelisted Numbers? * **Real-World Testing:** Experience firsthand how your agent interacts over a phone call. * **Seamless Integration:** Once added, your number appears in the test call dropdown, allowing you to trigger calls for any agent effortlessly. ### How to Add a Whitelisted Number 1. **Access the Whitelisted Page:** * In the sidebar, click on **Phone Numbers** and then select the **Whitelisted** page. 2. **Add Your Number:** * Click on **Add Number** in the top right. * Enter your phone number and assign it a friendly name for easy identification. * Click **Next**. 3. **Verification:** * An OTP (One-Time Password) will be sent to your phone. * Enter the OTP in the provided field and click **Verify**. 4. **Test Your Agent:** * Navigate to **Manage Agents** → select an agent → click on **Test**. * Click **Trigger Agent Call** and select your whitelisted number from the dropdown. * Click **Start Testing** and wait for your call. *Tip:* Keep your phone handy during testing, you’re about to experience your agent in action! # DTMF Collection Source: https://docs.gnani.ai/B01_DTMF Let your voice agents listen, capture, and respond to **keypad inputs** from users — essential for collecting sensitive inputs like phone numbers and PIN codes securely and efficiently. **DTMF (Dual-Tone Multi-Frequency)** support ensures your agents can handle numeric inputs with precision, whether it's authenticating users or routing calls based on input. ## What is DTMF and Why Enable It? DTMF is a system that sends numeric input over phone lines — like when you enter a PIN, press "1 for support", or key in an account number. With DTMF enabled, your voice agents can collect: * **Phone Numbers** * **PIN Codes** * **Account Numbers or OTPs** **Why it matters:** * **Secure Input:** Collect sensitive information without relying on voice transcription. * **Better UX:** Give users a familiar way to respond via dial pad. * **Essential for IVR:** Key component of any interactive phone workflow. * **Improves Accuracy:** Ideal for situations with background noise or low-quality audio, where speech recognition may struggle. ## How to Enable DTMF Support You’ll find a toggle under **Agent Settings → Customize → DTMF Collection**. > **Important:** Simply turning on the DTMF toggle does **not** activate the full flow.\ > To make it work, you must also update your **system prompt** using the correct DTMF signals as shown below. ## DTMF Collection Template Update your system prompt with the following format when collecting inputs. The signal format must be **precisely** followed. ### Collecting Phone Number When your agent asks for a phone number: * Append this signal: `| DTMF1010` * Ask the user clearly to provide input **after the beep** * Input must be a 10-digit number (e.g., `8197800293`) **System Prompt Example:** > "Can you please provide your phone number after the beep tone? | DTMF1010" **If the input is invalid:** > "That doesn't seem like a valid phone number. Please enter a 10-digit phone number after the beep. | DTMF1010" ### Collecting Pin Code When your agent asks for a PIN: * Append this signal: `| DTMF0610` * Input must be exactly 6 digits (e.g., `560033`) **System Prompt Example:** > "Can you please provide your pin code after the beep tone? | DTMF0610" **If the input is invalid:** > "That doesn’t seem like a valid pin code. Please enter a 6-digit pin after the beep. | DTMF0610" ## How DTMF Signals Work DTMF signals follow the format:\ `| DTMF[XXYY]`\ Where: * `XX` = Expected number of digits * `YY` = Time (in seconds) allowed for user input For example: * `DTMF1010` → Expect **10 digits**, allow **10 seconds** * `DTMF0610` → Expect **6 digits**, allow **10 seconds** > **Tip:** Always set the `YY` duration based on the TTS utterance length.\ > If the message is long but the time is short, the input might **timeout before the beep.** ## Bonus Tips & Best Practices | Tip | Why It Helps | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Match time to utterance length** | If the `YY` value (input time) is too short for your message, users won't be able to respond in time. | | **Don’t strip or reformat DTMF signals** | They must remain at the end of the prompt **exactly** as written. | | **Handle invalid input loops** | Guide the user to retry if their input doesn’t match the expected format. | | **Use clear instructions** | Mention “after the beep” to guide user expectations and ensure input is captured. | | **Enable barge-in (optional)** | If you're using TTS, enabling barge-in allows users to enter input **without waiting** for the entire prompt to finish. | | **Validate inputs post-call** | You can log or validate the collected digits after the call for reporting or verification workflows. | | **Use in noisy environments** | DTMF ensures input collection even when speech transcription is unreliable due to background noise. | By enabling and correctly using DTMF, you open up powerful, secure, and user-friendly workflows for your voice agents — from authentication to onboarding and beyond. Let your agents hear more than just voices. Let them listen to actions. # Advanced ASR Settings  Source: https://docs.gnani.ai/B02_Advanced_ASR Control how your voice agent listens, detects, and processes user speech in real-time conversations.  ### **Overview**  Advanced ASR (Automatic Speech Recognition) settings control how your AI voice agent listens and responds to spoken input.\ They define listening limits, silence detection, interruption behavior, and background noise filtering for optimal call experience. ### Location in Platform **Manage Agent → Customization → Advanced ASR Settings** > **Note:** These settings apply only to **voice channels**. They do not affect chat or text-based agents. ### **Available Settings**  | **Setting** | **Description** | | :-------------------------------------- | :----------------------------------------------------------------------------------- | | **Max Speech Duration** | Maximum duration (in seconds) the agent will listen to a single user input | | **Initial Silence Timeout** | Time to wait for the user to start speaking before cancelling | | **End Silence Timeout** | Time to wait after user stops speaking before finalizing the input | | **Speech Segmentation Silence Timeout** | Mid-speech silence duration used to split long speech into segments | | **Allow Interruptions** | Lets the user speak over the agent; the agent stops speaking and listens immediately | | **Interrupt Initial Message** | Allows interruptions during the agent’s very first message in a call or interaction | | **Background Noise Filtering** | Adjusts sensitivity to background sounds to reduce false interruptions | ## **How Each Setting Works**  ### **1. Max Speech Duration**  **Description:** Specifies how long the agent will listen to user input in one stretch before automatically stopping.  **Use Case:** Prevents prolonged listening due to background noise or over-talking. Ensures the agent maintains a responsive, controlled interaction.  ### **2. Initial Silence Timeout**  **Description:** Defines how long the agent will wait for the user to begin speaking at the start of a turn. If the user remains silent beyond this threshold, input is cancelled or retried.  **Use Case:** Useful when users are unsure, distracted, or take time to process the prompt. Prevents the system from hanging indefinitely.   ### **3. End Silence Timeout**  **Description:** Sets the duration of silence after the user stops speaking, after which the agent considers the input complete and proceeds.  **Use Case:** Allows for natural pauses while still keeping the interaction smooth. Essential for avoiding premature cutoff.  ### **4. Speech Segmentation Silence Timeout**  **Description:** When users speak in long sentences or paragraphs, this setting helps break the input into segments based on silence detection. Particularly useful for streaming ASR or multi-sentence inputs.  **Use Case:** Improves comprehension and reduces memory load on the model by processing inputs in manageable chunks.  ### **5. Allow Interruptions** **Description:** When enabled, the agent will stop speaking and immediately listen when the user starts talking.\ **Use Case:** Creates a more natural, back-and-forth conversation where users can cut in. ### 6. Interrupt Initial Message **Description:** When enabled (and **Allow Interruptions** is ON), the agent will also allow interruptions during its very first message.\ **Use Case:** Lets impatient users respond immediately, even during the greeting. ### 7. Background Noise Filtering **Description:** Controls how sensitive the interruption feature is to background sounds.\ Low sensitivity (closer to 20): More likely to trigger on quiet sounds, including unwanted noise.\ High sensitivity (closer to 100): Better at ignoring noise like traffic or barking but may miss very soft speech.\ **Use Case:** Reduce false triggers while balancing responsiveness. ### Recommendations * Use **shorter timeouts** for transactional bots (e.g., booking, verification). * Use **longer timeouts** for support scenarios, complex discussions, or with elderly users. * Enable **Segmentation** when expecting detailed or multi-part answers. * Enable **Allow Interruptions** for more natural, human-like interactions. * Adjust **Background Noise Filtering** based on the expected environment. # Voicemail Detection Source: https://docs.gnani.ai/B03_Voicemail Voicemails are a part of outbound calling but missed opportunities and awkward message drops don’t have to be. The **Voicemail Detection** feature helps your voice agent smartly identify when it’s reached a voicemail inbox and respond accordingly, avoiding awkward or wasted greetings, and optionally triggering automated follow-ups like SMS or email. This is especially useful in outbound call flows where pickup detection and timing are critical, such as **sales outreach**, **appointment reminders**, or **lead qualification**. **Path**: `Manage Agent → Customize tab → Voicemail Detection` ## Why Voicemail Detection Matters * Improve agent experience by avoiding cutoff intros or beeps. * Save on costs by skipping unnecessary conversation attempts. * Boost user trust with professional, voicemail-appropriate responses. * Automate engagement by triggering SMS, emails, or workflows after detection. ## What It Does When an outbound call is answered, your agent uses real-time audio cues to detect whether the recipient is a person or a voicemail system. Based on your configuration, it will either: * Retry voicemail detection, * Wait for the right moment to respond, * Speak a custom voicemail message, * Trigger automated follow-ups. ## What You Can Configure ### 1. Max Detection Attempts * **What it is**: Number of times the system retries to determine if the call went to voicemail. * **Why it matters**: Helps balance detection accuracy vs. speed and cost. * **Range**: 1 to 5 * **Example**: Setting this to 3 means the system will attempt detection up to 3 times before deciding. *Tip: Increase for better accuracy; decrease for faster calls and lower compute cost.* ### 2. Voicemail Playback Delay * **What it is**: Number of seconds the agent waits after voicemail is detected before speaking. * **Why it matters**: Prevents the agent from being cut off by long greetings or carrier beeps. * **Range**: 1 to 10 seconds * **Example**: A delay of 4 seconds gives enough time for most voicemail intros to end before the message plays. *Why delay?*\ Voicemail greetings can include:\ “Hi, this is John. I’m not available right now…” **\[beep]**\ If your agent starts too early, its message may be lost. ### 3. Voicemail Response * **What it is**: The message your agent will speak once voicemail is detected. You can also use dynamic variables in the message by using double curly braces like `{{customer_name}}` * **Limit**: 300 characters max * **Example**:\ `"Hi, we tried reaching you but missed you. Please call us back or reply to this message."` *Make it short, polite, and clear. Don’t include long messages. The goal is clarity, not conversation.* ### 4. Post Detection Actions * **What it is**: Optional actions your agent can take after leaving a voicemail. * **Examples**: * Send a follow-up SMS (e.g., "Just left you a voicemail. Text us back!") * Send an email summary to the contact owner * Trigger a custom API *Use this to automate re-engagement or log activity in CRMs or backend systems.* ## Bonus Tips & Best Practices | Tip | Why It Helps | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Keep initial greeting short | If the bot’s first message is too long, it might overlap with the phone’s voicemail greeting, making it impossible to capture user audio or transcripts. Keep it concise, especially in the first 5–10 seconds. | | Enable Barge-In Detection | Turn on Barge to let your agent listen while speaking. This improves voicemail detection accuracy, especially when voicemail systems start mid-sentence or play long intros. | | Personalize the voicemail message | A natural, human-sounding message improves callback rates. Add a name or reason for the call. | ## Example Scenario 1. Agent makes an outbound call. 2. No human detected → system retries 2 more times. 3. Detection succeeds on attempt 3. 4. Agent waits 5 seconds → speaks voicemail message. 5. Triggers CRM update. ## When Should You Use It? * When your outbound calls frequently hit voicemail. * If you want to leave clean, professional messages without being cut off. * When you want to automate follow-up actions after voicemails. * To improve response rates by sending SMS/email right after missed calls. ## What This Feature Does Not Do (Yet) * It doesn’t guarantee 100% voicemail detection. * It won’t auto-detect specific voicemail messages (e.g., “the mailbox is full”). ## Best Practices | Goal | Setting Recommendation | | ----------------------- | -------------------------------------- | | Max accuracy | Max Attempts: 5, Delay: 4–5s | | Faster call completions | Max Attempts: 1–2, Delay: 2–3s | | Better callback rate | Always add Post Detection SMS or Email | | Avoid being cutoff | Use a delay of at least 3s | ## Troubleshooting | Issue | What to check | | ---------------------- | ----------------------------------------------------- | | Agent speaks too early | Increase Voicemail Playback Delay | | Message not played | Check if detection attempt limit was too low | | No follow-up actions | Ensure Post Detection Actions are configured properly | # Dynamic Variables & Dynamic Messages Source: https://docs.gnani.ai/B04_Dynamic_Variables Personalize your agent's responses The **Dynamic Variables** feature in Gnani Agents allows you to insert placeholders in your bot’s **Greeting Message** or **System Prompt**, which get replaced with real-time data during a call.\ These variables are populated from an API configured in the **Dynamic Messages** settings. *** ### **Dynamic Messages Overview** When you turn on **Dynamic Messages** in the agent settings, you will be asked to configure: * **Method** — `GET` or `POST` (required) * **API URL** — endpoint that returns the dynamic message and user context (required) * **Headers** — optional key–value pairs (if applicable) This API request will contain: 1. A **unique variable** (for example, the customer’s phone number). 2. Return: * **User context** — any key–value data needed for the call (e.g., `due_amount`, `customer_type`, `due_date`) in JSON format. * **Constructed greeting message** — the exact greeting to be used for that specific call. **Important:** * The Greeting Message configured in the agent will be **overridden** by the message returned from your API when Dynamic Messages is on. * The API call has a **10-second timeout**. If it doesn’t respond in time, the call will fail. * Currently, Gnani Agents does **not** have a built-in campaign manager. This feature is available only by contacting us to enable it for your deployed bot. We’re working to make it self-service soon. * The **unique variable** can be something other than the phone number. *** ### Pre-call variables Define caller-specific data before each call so the agent starts with context instead of asking redundant questions. | Setting | Description | | --------------------- | ------------------------------------------------------------------------------------------------------- | | **Unique identifier** | One variable used as the primary key per call (for example, phone number or customer ID) | | **Variable** | Named placeholder stored from your API or campaign data (for example, `customer_name`, `policy_status`) | | **Description** | Short note for the LLM explaining what the variable represents | Use variables in the **Greeting Message**, **Ending Message**, and **System Prompt** with double curly braces: ```text theme={null} Hi {{customer_name}}, I'm calling from Gnani Technical Support. ``` During test calls, enter sample values manually. In production, values are filled from your Dynamic Messages API response. *** ### **Using Dynamic Variables in the System Prompt** Dynamic variables can also be used inside the **System Prompt**, but: 1. **Dynamic Messages** must be turned on. 2. In the **Customize** section, enable **Pre-Call Variables**. 3. Add the required variables you plan to use in your System Prompt. **Testing behavior:** * During test calls, you’ll be prompted to manually enter the test variable values. * In production, these will be **auto-filled** from your Dynamic Messages API response. *** ### **Where Dynamic Variables Can Be Used** **Supported:** * Greeting Message (overridden by API response) * System Prompt **Not supported:** * Ending Message *** ### **API Specification** **Request Body** When the API is called, Gnani Agents sends: ```json theme={null} { "conversation_id": "abc123", "mobile": "01234567890" } ``` **Expected Response** ```json theme={null} { "additional_info": { "inya_data": { "text": "Hi John Doe, your payment of ₹2500 is due on 2025-08-20.", "user_context": { "phone_number": "+911234567890", "name": "John Doe", "due_amount": "2500", "due_date": "2025-08-20", "customer_type": "Premium" } } } } ``` * `text` → Greeting message to be used for the call (mandatory). * `user_context` → Key–value pairs for use in the System Prompt (mandatory; user\_context can be an empty dictionary if no dynamic variables are used). *** ### Sample API in Python ```python theme={null} @router.post("/initial_message") async def bot_initial_message(request: Request): body = await request.json() mobile = body.get("mobile") # Example: fetch data from your CRM user_data = crm_query(mobile) response_data = { "additional_info": { "inya_data": { "text": "Hi {user_data.get('name')}, this is a test message", "user_context": { "phone_number": mobile, "name": user_data.get("name"), "age": user_data.get("age") } } } } # Status code 200 → call will be triggered # Status code 400 → call will NOT be triggered return JSONResponse(content=response_data, status_code=200) ``` > This API can be built in any language. The above is a **Python** example. *** ## Best Practices * Keep API latency low to avoid hitting the 10-second timeout. * Return only the variables you need to keep payloads small. * Always validate that your API returns both `text` and `user_context`. * Use consistent naming for variables so they can be reliably inserted into prompts. # Writing a Disposition Prompt Source: https://docs.gnani.ai/C01_Disposition Configure call dispositions and write prompts that categorize call outcomes for analytics. ## Configure call dispositions Call dispositions categorize the outcome of each agent call for reporting in **Agent Analytics** and **Conversational Logs**. ### Enable dispositions 1. Open your agent in **Manage Agents**. 2. Go to the **Analytics Config** tab. 3. Toggle **Call Dispositions** on. 4. Enter a **default prompt** that instructs the LLM how to classify calls. 5. Click **Add Disposition** for each category you need. Each disposition includes: | Field | Purpose | | ---------- | --------------------------------------------------------------- | | **Name** | Label shown in analytics (for example, `Resolved`, `Escalated`) | | **Prompt** | Conditions under which a call should receive this disposition | Once configured, the platform assigns dispositions automatically after each call. View results in [Agent Analytics](/E03_Agent_Analytics) and [Conversational Logs](/E01_Conversational_Logs). For prompt structure and field definitions, see the sections below. *** ## Disposition prompt structure Given below is the structure of the disposition prompt. Follow the structure to craft your own call-transcript extraction prompts: ### 1. Define the Role and Context * **What does it do?**\ Specify who the LLM is and the domain it operates in (e.g., Call Center Operations Analyst in Debt Collection). * **Why it matters:**\ Sets expectations and tailors the model's responses. ### 2. Clarify the Task * **What does it do?**\ Write a brief `"## Task"` section that explains exactly what the model should accomplish (e.g., read transcript, extract fields). * **Why it matters:**\ Keeps the model focused on the end goal. ### 3. Emphasize Output Requirements * **What does it do?**\ Under `"## IMPORTANT"`, stress the format (JSON) and forbid extra fields or custom codes. * **Why it matters:**\ Ensures structured, machine-readable output. ### 4. Provide Business and Language Context * **What does it do?**\ Use `"## Context Understanding"`, `"## Business Context"` and `"## Language Processing Guidelines"` sections to share domain rules, use cases, and allowed languages. * **Why it matters:**\ Guides the model on tone, terminology, and multilingual handling. ### 5. List Field Definitions * **What does it do?**\ Create a JSON snippet showing each key with an empty value. * **Why it matters:**\ Shows exactly which fields to populate and in what structure. ### 6. Detail Allowed Values * **What does it do?**\ Under `"### Allowed Values & Definitions"`, enumerate each field's valid codes, descriptions, and criteria in priority order for STAGE\_CODE. * **Why it matters:**\ Prevents misclassification and enforces consistency. ### 7. Add Critical Analysis Instructions * **What does it do?**\ Numbered guidelines on transcript reading, stage determination, priority rules, and callback handling. * **Why it matters:**\ Helps users correctly apply the template and avoid common mistakes. ### 8. Show Output Format Example (Optional) * **What does it do?**\ Provide a sample JSON response matching the field definitions. * **Why it matters:**\ Offers a quick reference for the expected output. ### 9. Insert the Transcript Placeholder * **What does it do?**\ End with a `"## Transcription"` header where the actual call log goes. * **Why it matters:**\ Clearly demarcates where to paste the raw data. *** ## Tips * Copy the template and fill in the `{{placeholders}}` with your specific details. * Use simple, clear language when defining the call purpose and rules. * Don't remove or rename any keys in the JSON snippet, they must match exactly. ### Example Prompt Template ````mdx theme={null} ## Role You are a skilled Call Center Operations Analyst specializing in {{Industry}} operations. You will be given call logs that contain detailed conversation transcripts between an Agent and a User. The call transcripts could be in {SUPPORTED_LANGUAGES} or mixed language. ## IMPORTANT Provide your response strictly in **JSON format** following the specifications below. Do not introduce any additional fields or custom stage codes beyond those defined. ## Task Read the entire conversation transcript carefully and extract the required information according to the rules. Deliver a structured JSON response containing only the specified keys. ### Context Understanding - **Call Purpose**: {{Call Purpose}} - **Participant Roles**: Agent and User ### Business Context - **Industry**: {{Industry}} - **Use Case**: {{Use Case}} - **Business Rules (Optional)**: - 1. {{Rule 1}} - 2. {{Rule 2}} ### Language Processing Guidelines - **Primary Language**: {{Primary language}} - **Secondary Languages (Optional)**: {{Secondary language (if applicable)}} ## Specifics ### Field Definitions ```json { "STAGE_CODE": "" } ``` ### Allowed Values & Definitions #### STAGE\\\_CODE Values: ```json [ { "code": "ESCALATED_TO_AGENT", "description": "Transferred to human agent for complex issues", "criteria": "User requests escalation or dispute is detected." }, { "code": "AGREES_FOR_CALLBACK", "description": "User agrees to a callback", "criteria": "User explicitly agrees to be called back." }, { "code": "DISAGREES_FOR_CALLBACK", "description": "User declines a callback", "criteria": "User explicitly declines being called back." }, { "code": "BUSY", "description": "User indicates they are busy", "criteria": "User says they cannot talk now." }, { "code": "FAQ_HANDLED", "description": "User question answered without escalation", "criteria": "User asks a routine question and receives answer." }, { "code": "NO_INPUT", "description": "No user response after prompt", "criteria": "Silence or no intelligible input." }, { "code": "INVALID_INPUT", "description": "Unrecognized or unclear response", "criteria": "User input is garbled or irrelevant." }, { "code": "WRONG_NUMBER", "description": "Wrong number reached", "criteria": "User indicates wrong number." }, { "code": "DND", "description": "Do-not-disturb request", "criteria": "User requests not to be contacted again." } ] ``` ## Critical Analysis Instructions 1. Read the **ENTIRE transcript** before extracting. Don't jump to conclusions 2. **Look for explicit user responses** - Don't assume agreement from silence ## Important Guidelines ### Data Extraction Rules - **Key Restriction**: Extract only the keys specified in the field definitions. Do not add any additional keys or fields to the output. - **Output Compliance**: Follow the exact JSON format specified. The output must contain only the fields defined in the field definitions section. ## Output Format Provide response in JSON format. ### Standard Response Structure - json ``` { "STAGE_CODE": "{EXTRACTED_VALUE}" } ``` ## Transcription \ ```` *** # Language Switch Prompt Source: https://docs.gnani.ai/C02_Language_Switch Configure how your agent detects and responds to user requests for changing languages during a conversation. ### **Overview** The Language Switch Prompt enables your AI agent to dynamically identify and respond when a user explicitly or implicitly requests a switch in language. This functionality is critical for agents that support multilingual conversations, especially over voice where real-time language adaptation enhances usability and reach. ### **Purpose** When multilingual agents are enabled, this prompt instructs the system to: 1. Detect whether the user is requesting a language change. 2. If detected, identify the specific target language. 3. Return the correct output so that downstream logic can route accordingly. ### **Where It Lives** **Location:**\ Manage Agents → Overview → Language Switch Prompt Whenever you **add or remove a language**, the system prompts you to update this field to ensure alignment between what the agent supports and what it can detect. A pre-filled **reference prompt** is provided. You are expected to customize it based on the languages added to your agent and their corresponding trigger phrases. ### **Why It’s Important** Language switching relies on both **system configuration** and **prompt logic**. If a language is added to the agent configuration but not reflected in the prompt (or vice versa), switching will fail at runtime. The prompt becomes the source of truth for how language detection is handled across STT engine outputs.  ### **Prompt Template (To Customize)** Below is the reference structure you should adapt based on your agent’s supported languages and expected phrases. ```markdown theme={null} ## Role: - You are a specialized intent detection agent designed to analyze outputs from multiple Speech-to-Text (STT) engines and identify if the user is explicitly requesting to switch or change languages. ## Objective: - Determine if the user is requesting a language switch by examining transcriptions from multiple STT engines. - If a language switch intent is detected, identify the specific language the user wants to switch to. ## Input Format: - S1: Engine 1 Output (can be in any script/language) - S2: Engine 2 Output (can be in any script/language) - S3: Engine 3 Output (can be in any script/language) ## Output Format: - If a language switch intent is detected, output ONLY the target language name. - If no language switch intent is detected, output ONLY "None". - Valid outputs: , , ......... "None" (or other languages as specifically mentioned) ## Analysis Process: * Language Switch Intent Detection: - Check each engine output for explicit requests to change languages such as: - English phrases: "switch to [language]", "change language to [language]", "I want to speak in [language]", "let's talk in [language]" - Hindi phrases: "भाषा बदलें [language]", "मैं [language] में बात करना चाहता हूं", "चलो [language] में बात करें" . . . . - - Look for language-specific switch indicators: - Words like "switch", "change", "speak in", "talk in" followed by a language name - Similar phrases in other languages that indicate a desire to change languages - For partial or unclear requests, cross-reference all STT outputs to determine the intent * Target Language Identification: - If a switch intent is detected, identify the target language requested: - For direct mentions: Extract the language name from the phrase - For implicit mentions: Infer from context which language is being requested b. Only consider these valid target languages (unless others are specifically mentioned): - - . . - - Decision Rules: a. If ANY of the engine outputs contain a clear language switch request: Output the requested language name b. If NONE of the engine outputs contain a language switch request: Output "None" c. If multiple conflicting language switches are requested: Output the language that appears most consistently across engines Sample Input/Output Examples: Example 1: Input: S1: "ಮುಂದೆ ಕನ್ನಡದಲ್ಲಿ ಮಾತನಾಡೋಣ" S2: "let's speak in Kannada" S3: "ನಾವು ಕನ್ನಡದಲ್ಲಿ ಮಾತನಾಡೋಣ" Output: Kannada Example 2: Input: S1: "తెలుగులో మాట్లాడుదాం" S2: "let's talk in Telugu" S3: "నేను తెలుగులో మాట్లాడాలనుకుంటున్నాను" Output: Telugu IMPORTANT: The output must ONLY contain the target language name or "None". Do not include any analysis, explanation, or other text in the output. ``` ### **Trigger Workflow** When a new language is added to the agent via the UI: 1. A **confirmation dialog** is shown instructing you to update the Language Switch Prompt. 2. Navigate to Agent Overview → Language Switch Prompt. 3. Modify the reference prompt to include: 1. Any new language added 2. Detection examples/phrases (if required) 4. Save and deploy. Failure to update this prompt will result in language switching not working, even if the language has been configured elsewhere. ### **Best Practices** | **Practice** | **Recommendation** | | :----------------- | :-------------------------------------------------------------------------------------------------- | | Prompt Matching | Always ensure the languages listed in the prompt match the ones added to the agent’s system prompt. | | Script Awareness | Include detection patterns in local scripts (e.g., Hindi, Kannada) if relevant. | | Clear Output | Always return a clean one-word output. Avoid additional logs, quotes, or structures. | | Prompt Maintenance | Revisit and update the prompt whenever you add, remove, or rename any supported languages. | | Sample Inputs | Use actual transcriptions from user speech; avoid manually translated phrases for accuracy. |   # Using Jinja for Dynamic System Prompts  Source: https://docs.gnani.ai/C03_Jinja Use Jinja2 templates in system prompts for dynamic, caller-specific agent behavior. ### **Overview**  The Jinja templating feature allows you to create powerful, dynamic System Prompts. By using variables that are resolved with real-time data before a call begins, you can build personalized and context-aware conversations. This guide explains how to use Jinja templates, the validation rules in place, and the benefits of this approach.  ## **Core Concept: Understanding Variable Types**  Our system uses two distinct types of curly braces to handle variables from different sources. Understanding this distinction is crucial for building prompts correctly.  ### \*\*1. Server-Side Variables: \*\* These variables are placeholders for data that you provide to the system *before* the call is initiated.  * **Source:** The value for these variables must come from one of two places:  * **Dynamic API Response:** Data fetched from your API endpoint just before the call.  * **Pre-call Variables:** Data you provide when triggering the call via an API, especially during testing.  * **Use Case:** Ideal for injecting customer-specific data like names, appointment details, order history, or account status.  * **Example:** Hello customer\_name , your appointment is scheduled for booking\_date .  ### \*\*2. User-Input Variables: \*\* These variables are placeholders for information that is extracted directly from the **user's spoken message** during the conversation.  * **Source:** The value is populated by the system based on the user's utterance.  * **Use Case:** Perfect for capturing dynamic user choices or inputs within a conversation turn.  * **Example:** If the prompt is “To confirm, you selected user\_selected\_option, right?”, the value of user\_selected\_option will be filled from what the user said in their previous turn.  ### ***Variable Reference Table*** | Syntax | Example | Source of Value | Required in API/Pre-call Variables? | | :------------- | :--------------------- | :----------------------------------------- | :---------------------------------- | | variable\_name | customer\_name | Dynamic API response or Pre-call variable | **Yes** | | variable\_name | user\_selected\_option | Extracted directly from the User's Message | **No** | ### **Validation Rules and System Behavior**  To ensure your prompts are reliable, the system validates the Jinja template syntax before you can save or test an agent.  ***Validation Checks:*** The System Prompt is validated for the following conditions:  * **Correct Jinja Syntax:** Ensures all brackets and statements (e.g., `{{var}}`, `{ %if% }`) are correctly formatted.  * **No Undeclared Variables:** All variables wrapped in double curly braces `{{...}}` must have a corresponding value provided either from the dynamic API or pre-call variables.  ### ***Error Handling*** The system's behavior changes based on the validation result:  | Condition | Save / Test Buttons | System Message | | :------------------------------------------------------------------ | :------------------- | :----------------------------------------------- | | **Valid Syntax** | Enabled | – | | **Invalid Jinja Syntax** | Disabled | "Invalid Jinja syntax." | | **Variable Missing at Runtime** (Variable not in API/pre-call data) | Enabled (save works) | "Call triggered failed." (error during the call) | ### ***Agent Behavior*** * **Existing Agents:** Agents created before this feature will continue to function as normal until their System Prompt is modified and saved again.  * **New & Updated Agents:** All new agents, or existing agents whose prompts are updated, must follow the Jinja formatting and validation rules.  ### **Examples**  **1. Valid Syntax**  ```html theme={null} Hello {{customer_name}}, your booking for a {{product_name}} is confirmed for {{booking_date}}.  ``` **2. Invalid Syntax**  ```html theme={null} Hello {{customer_name}}. Your booking is on {{booking_date}.   ``` * **Error:** The curly braces for booking\_date are not closed.  * **System Behavior:** The Save and Test buttons will be disabled until the syntax is corrected.  **3. Advanced Example with Conditional Logic**  ```html theme={null} Hello {{ customer_name }}.   {% if loyalty_status == 'Gold' %}  As a Gold member, you get a special 20% discount.   {% elif loyalty_status == 'Silver' %}  As a Silver member, you get a 10% discount.  {% else %}  Thank you for being our customer.  {% endif %}  Your order #{{ order_id }} is ready.   ``` *In this example, the agent's greeting is personalized based on the customer's loyalty\_status variable.*  ### **How Jinja Optimizes Cost and Performance**  Using Jinja templates, especially with conditional blocks (`{% if %}`, `{% elif %}`, `{% else %}`), significantly optimizes your prompts.  * **Reduces Input Tokens:** By including only the relevant text based on the provided variables, the overall length of the prompt sent to the language model is reduced.  * **Lowers Inference Costs:** Fewer input tokens directly translate to lower API costs for each call.  * **Improves Execution Speed:** Shorter, more concise prompts are processed faster by the language model, reducing latency.  **Important Note:** During **testing mode**, if a variable exists in both the pre-call variables and the dynamic API response, the value from the **pre-call variables will be used**.  # SMS Integration and Action Source: https://docs.gnani.ai/D01_Twilio_SMS Enable your agent to send SMS notifications like appointment reminders, ticket confirmations, order updates, discount codes, and more — all directly from Gnani Agents using Twilio. *** ### **Why Integrate SMS?** With SMS integration, your agent can: * Send **appointment reminders** to reduce no-shows. * Deliver **order updates** and **ticket confirmations** instantly. * Share **promotional offers** or **discount codes** with customers. *** ## 1. Steps to Integrate 1. **Open Integration Settings** * Go to \*\*Integration \*\*section in your Manage Agent page. * Add a new integration. Select SMS. * Select **Twilio** as the operator and enter a name for your integration. 2. **Get Twilio Credentials** * Open the [Twilio Console Dashboard](https://console.twilio.com/dashboard) to retrieve credentials. * Copy your **Account SID** (34- character alphanumeric identifier starts with `AC`) and **Auth Token** from the **Account Info** section. * Paste these into the integration form in Gnani Agents. 3. **Choose Sender Type** \*\*Option A — Phone Number \*\*to send messages from a fixed number for consistent branding. * In Twilio: Go to **Develop → Phone Numbers → Manage → Active Numbers**. * Select an active **SMS-enabled** number and paste it into Gnani Agents. * If you don’t have one, click **Buy a Number** in Twilio. \*\*Option B — Messaging Service \*\*to use a pool of numbers for sending SMS dynamically based on factors like location and compliance. * In Twilio: Go to **Develop → Messaging → Services**. * Copy the **Messaging Service SID** (starts with `MG`) and paste it into Gnani Agents. * If you don’t have one, click **Create Messaging Service** in Twilio. 4. **Save** * Click **Save** to complete the integration. *** ## **2. Create an SMS Action** 1. **Navigate to Actions** * Go to **Manage Agents → Select Agent → Action Tab**. 2. **Create Action** * Click **+** to open the **Create Action** card. 3. **Fill in Details** * **Name:** Use only letters, numbers, or underscores. * **Description:** Describe when the action should trigger. * Example: *"Trigger this action when the user asks about available offers."* 4. **Select Integration** * Choose the Twilio SMS integration you set up earlier. 5. **Choose Trigger** * **Post-Call:** Executes after the conversation ends. * If **Post-Call** is selected, you can: * Add **Variables** (sent to the API). * **On-Call:** Executes during the conversation. * If **On-Call** is selected, you can: * Write a message in the **Speak During Action** section (what the agent says when executing). * Add **Before API Call Variables** (sent to the API). Create new variables or reuse existing ones to store dynamic values like names, order IDs, or dates. * Add **After API Call Variables** (received from the API). 6. **Message Template** * Enter the SMS content to send. Use variables for dynamic content. * Example: `Hello {{user_name}}, your order {{order_id}} is on the way! ` *** ## **3. Test Your SMS Action** Before using the action in real conversations, test it to ensure it’s working correctly. 1. **Open Testing Mode** * Go to **Manage Agents → Select Agent → Test**. 2. **Start a Test Conversation** * Speak or chat with your agent inside the test interface. 3. **Trigger the Action** * Ask the agent for similar instructions or keywords mentioned in the Action’s **Description**. * Example: If your description says *"Trigger this when user asks about offers"*, say **"What offers do you have?"**. 4. **Check SMS Delivery** * Confirm the SMS arrives at the configured number. * Ensure placeholders (e.g., `{{user_name}}`) are replaced with actual values. 5. **Troubleshoot if Needed** * Click on the more options on your created action card to see the **View Logs** option. You can check the **Action Logs** here. * Verify Twilio credentials and sender configuration. * Make sure the test number is SMS-enabled. * Review triggers, message template, and variables for errors. *** **You’re ready!** Your agent can now send automated, personalized SMS messages during or after calls. # CRM Integration and Action Source: https://docs.gnani.ai/D02_Zoho_CRM Integrate Zoho CRM with Gnani Agents to automatically create and manage customer records during or after conversations — making lead capture and support tracking seamless. *** ## **1. Why Integrate Zoho CRM?** By connecting Zoho CRM to Gnani Agents, your agent can: * **Automatically create leads, contacts, or deals** while talking to customers. * Reduce manual data entry by **capturing customer details in real time**. * Improve lead follow-up speed and support resolution times. *** ## **2. Set Up Zoho CRM Integration** 1. **Open Integration Settings** * Go to **Add CRM Integration** in your Gnani Agents dashboard. * Select **Zoho** as the operator and enter a name for your integration. 2. **Get Zoho Credentials** * Open the Zoho API Console. * Create a **Self Client** by following Zoho’s Self Client guide. 3. **Copy Client ID & Client Secret** * In the **Client Secret** tab, locate your **Client ID** and **Client Secret**. * Paste both into the integration form in Gnani Agents. 4. **Enter Account Server URL** * Choose the correct **Account Server URL** based on your primary region from Zoho’s region list. 5. **Generate Account SOID** * Follow **Step 1** in this Zoho guide to generate your **Account SOID**, then paste it into Gnani Agents. 6. **Specify Required Scopes** * From Zoho’s scope list, add the required scopes for your action (e.g., `ZohoCRM.modules.ALL`). 7. **Save** * Click **Save** to complete the integration. *** ## **3. Create a CRM Action** 1. **Navigate to Actions** * Go to **Manage Agents → Select Agent → Action Tab**. 2. **Create Action** * Click **+** to open the **Create Action** card. 3. **Fill in Details** * **Name:** Use only letters, numbers, or underscores. * **Description:** Describe when the action should trigger. * Example: *"Trigger this action when the user provides new lead details."* 4. **Select Integration** * Choose your **Zoho CRM** integration. 5. **Choose Trigger** * **Post-Call:** Executes after the conversation ends. * If **Post-Call** is selected, you can: * Add **Variables** (sent to the API). * **On-Call:** Executes during the conversation. * If **On-Call** is selected, you can: * Write a message in the **Speak During Action** section. * Add **Before API Call Variables** (sent to the API). * Add **After API Call Variables** (received from the API). 6. **Set CRM-Specific Fields** * **Select Action:** Choose the CRM action type (e.g., `CREATE` for adding new records). * **Module Name:** Specify the CRM module where the record should be created (e.g., `Leads`, `Accounts`, `Contacts`, `Deals`). * **Payload:** Define the data in JSON format. Example: ```json theme={null} { "Company":"Example Corp", "Name": "Smith", "Email": "john.smith@example.com" } ``` *** ## **4. Test Your CRM Action** Testing ensures the integration works correctly before using it in live conversations. 1. **Open Testing Mode** * Go to **Manage Agents → Select Agent → Test**. 2. **Start a Test Conversation** * Chat or speak with your agent in the test interface. 3. **Trigger the Action** * Use the same instructions or keywords from your Action’s **Description** to activate it. 4. **Verify in Zoho CRM** * Log in to Zoho CRM and check if the record was created or updated as expected. 5. **Check Action Logs** * In Gnani Agents, click **More Options (⋮)** on the Action card → **View Logs**. * Review execution details and any API errors. *** **Your Zoho CRM integration is now ready!** Your agent can capture leads and customer details without manual effort. # Email Integration and Action Source: https://docs.gnani.ai/D03_MailSend_Email Allow agents to send emails Integrate your email provider with Gnani Agents to automatically send personalized emails — such as welcome messages, follow-ups, newsletters, and transactional notifications — without manual effort. *** ## **1. Why Integrate Email?** By connecting an email service to Gnani Agents, your agent can: * **Send automated welcome emails** when a new customer signs up. * Deliver **follow-up sequences** after calls. * Share **newsletters, offers, or promotions** at the right moment. * Trigger **transactional emails** (order confirmations, invoices, etc.) instantly. *** ## **2. Set Up Email Integration** You can integrate with either **Mailchimp** or **SendGrid** depending on your preference. *** ### **Option 1 — Mailchimp Integration** 1. **Open Integration Settings** * Go to **Add Email Integration** in your Gnani Agents dashboard. * Select **Mailchimp** as the operator and provide an integration name. 2. **Get Your API Key** * Log in to your Mailchimp account. * Go to **Account → Extras → API Keys**. * Create or copy an existing **API Key**. 3. **Add API Key to** Gnani Agents * Paste the Mailchimp API Key into the integration form in Gnani Agents. 4. **Save** * Click **Save** to complete the integration. *** ### **Option 2 — SendGrid Integration** 1. **Create or Log Into Your Account** * Go to [SendGrid](https://sendgrid.com) and sign up or log in. 2. **Create an API Key** * In your SendGrid dashboard, go to **Settings → API Keys**. * Click **Create API Key**. * Name it (e.g., *Platform Integration*). * Set **API Key Permissions** to **Full Access** or at least **Mail Send**. * Click **Create & View** — copy this key (you won’t see it again later). * Paste it into Gnani Agents' **API Key** field. 3. **Add & Verify Your Sender Email** **Option A — Single Sender Verification (Quick)** * Go to **Settings → Sender Authentication → Verify Single Sender**. * Enter your sender email (e.g., `support@yourdomain.com`) and details. * Click the verification link sent to your email. * Use this sender email in Gnani Agents. **Option B — Domain Authentication (Recommended for Production)** * If you manage your domain’s DNS, choose **Domain Authentication**. * SendGrid will give you **CNAME records** to add to your DNS. * Once verified, you can send from any address on that domain. 4. **Save** * Click **Save** to complete the integration. *** ## **3. Create an Email Action** 1. **Navigate to Actions** * Go to **Manage Agents → Select Agent → Action Tab**. 2. **Create Action** * Click **+** to open the **Create Action** card. 3. **Fill in Details** * **Name:** Use only letters, numbers, or underscores. * **Description:** Describe when the action should trigger. * Example: *"Trigger this action when the user provides their email for newsletter signup."* 4. **Select Integration** * Choose your Mailchimp or SendGrid integration. 5. **Choose Trigger** * **Post-Call:** Executes after the conversation ends. * If **Post-Call** is selected, you can: * Add **Variables** (sent to the API). * **On-Call:** Executes during the conversation. * If **On-Call** is selected, you can: * Write a message in the **Speak During Action** section. * Add **Before API Call Variables** (sent to the API). * Add **After API Call Variables** (received from the API). 6. **Set Email-Specific Fields** * **Select Action:** Select the default option *(Contact us for support for multiple Mailchimp actions)* * **Template Name:** Select a Mailchimp email template to use. * **From Name & Email:** The verified sender name and email in Mailchimp. * **To Name & Email:** The recipient’s details. Variable input is recommended (e.g., `{{customer_name}}`, `{{customer_email}}`). * **Add Subject:** Custom subject line — can include variables. * **Merge Variables:** Map Gnani Agents variables to Mailchimp template placeholders: * **Key:** Variable name in Mailchimp template. * **Value:** Corresponding Gnani Agents variable (e.g., `{{order_id}}`). *** ## **4. Test Your Email Action** Testing ensures your emails are triggered correctly before going live. 1. **Open Testing Mode** * Go to **Manage Agents → Select Agent → Test**. 2. **Start a Test Conversation** * Speak or chat with your agent in the test interface. 3. **Trigger the Action** * Use the same instructions or keywords from your Action’s **Description** to activate it. 4. **Check Your Email** * Verify that the email is received by the intended recipient. * Confirm that template variables are replaced correctly with actual data. 5. **Check Action Logs** * In Gnani Agents, click **More Options (⋮)** on the Action card → **View Logs**. * Review the logs for execution details and any API errors. *** **Your Email integration is now ready!** Your agent can send beautifully designed, personalized emails using Mailchimp or SendGrid directly from conversations. # Ticketing Integration and Actions Source: https://docs.gnani.ai/D04_Zoho_Ticket Integrate Zoho Desk with Gnani Agents to automatically create and manage support tickets — helping your team respond faster and resolve customer issues efficiently. *** ## **1. Why Integrate Ticketing?** By connecting Zoho Desk to Gnani Agents, your agent can: * **Automatically log customer issues** during or after a call. * Ensure no support requests are missed. * Reduce manual ticket creation time for your support team. *** ## **2. Set Up Zoho Desk Integration** 1. **Open Integration Settings** * Go to **Add Ticket Integration** in your Gnani Agents dashboard. * Select **Zoho** as the operator and enter an integration name. 2. **Get Zoho Credentials** * Open the Zoho API Console. * Follow Zoho’s Self Client guide to create a Self Client. 3. **Copy Client ID & Client Secret** * In the **Client Secret** tab, locate your **Client ID** and **Client Secret**. * Paste both into the integration form in Gnani Agents. 4. **Enter Account Server URL** * Choose the correct **Account Server URL** based on your primary region from Zoho’s region list. 5. **Generate Account SOID** * Follow this Zoho guide to generate your **Account SOID** and paste it into Gnani Agents. 6. **Specify Required Scopes** * From Zoho Desk’s scope list, add the scopes needed for your ticket actions (e.g., `ZohoDesk.tickets.CREATE`). 7. **Save** * Click **Save** to finalize the integration. *** ## **3. Create a Ticket Action** 1. **Navigate to Actions** * Go to **Manage Agents → Select Agent → Action Tab**. 2. **Create Action** * Click **+** to open the **Create Action** card. 3. **Fill in Details** * **Name:** Use only letters, numbers, or underscores. * **Description:** Describe when the action should trigger. * Example: *"Trigger this action when the user reports a technical issue."* 4. **Select Integration** * Choose your Zoho Desk integration. 5. **Choose Trigger** * **Post-Call:** Executes after the conversation ends. * If **Post-Call** is selected, you can: * Add **Variables** (sent to the API). * **On-Call:** Executes during the conversation. * If **On-Call** is selected, you can: * Write a message in the **Speak During Action** section. * Add **Before API Call Variables** (sent to the API). * Add **After API Call Variables** (received from the API). 6. **Set Ticketing-Specific Fields** * **Select Action:** Choose the ticketing action type (e.g., `CREATE` for new tickets). *(Contact us to add more actions)* * **Department ID:** Specify the Zoho Desk department for ticket creation. * **Contact ID:** Associate the ticket with a customer profile. *(Currently for testing; contact us to make it dynamic)* * **Payload:** Define the ticket in JSON format. The **subject**, **description**, and **priority** are mandatory: ```json theme={null} { "subject": "{{subject}}", "description": "{{description}}", "priority": "{{priority}}" } ``` *** ## **4. Test Your Ticket Action** 1. **Open Testing Mode** * Go to **Manage Agents → Select Agent → Test**. 2. **Start a Test Conversation** * Speak or chat with your agent in the test interface. 3. **Trigger the Action** * Use the same instructions or keywords from your Action’s **Description** to activate it. 4. **Verify in Zoho Desk** * Check Zoho Desk to confirm that a ticket is created with the correct subject, description, and priority. 5. **Check Action Logs** * In Gnani Agents, click **More Options (⋮)** on the Action card → **View Logs**. * Review logs for execution details and any errors. *** **Your Zoho Desk integration is now ready!** Your agent can log support tickets instantly, ensuring fast and efficient customer service. # Custom Integrations and Actions Source: https://docs.gnani.ai/D05_Custom Build custom HTTP integrations to connect your agent with external APIs and services. Custom integrations allow you to connect **any external API** to your Gnani Agents — perfect for unique workflows and advanced automation that go beyond built-in integrations. *** ## **1. Why Use Custom Integrations?** With a custom integration, you can: * Connect your agent to any API endpoint you control or have access to. * Extend agent capabilities to trigger workflows in other systems. * Handle use cases not covered by predefined integrations. *** ## **2. Set Up a Custom Integration** 1. **Open Integration Settings** * Go to **Add Custom Integration** in your Gnani Agents dashboard. 2. **Enter Basic Details** * **Integration Name**: Use letters, numbers, or underscores only. * **Description**: Briefly explain the integration’s purpose. 3. **Choose API Call Method & URL** * Select the **Method** (e.g., `GET`, `POST`, `PUT`, `DELETE`). * Enter the **API URL** where the request should be sent. 4. **Add Authentication (If Needed)** * If your API requires authentication, enter the **Key** and **Value**. * Example: `Key: Authorization Value: Bearer ` 5. **Save Integration** * Click **Integrate** to complete the setup. *** ## **3. Create a Custom API Action** 1. **Navigate to Actions** * Go to **Manage Agents → Select Agent → Action Tab**. 2. **Create Action** * Click **+** to open the **Create Action** card. 3. **Fill in Details** * **Name:** Use only letters, numbers, or underscores. * **Description:** Describe when the action should trigger. * Example: *"Send lead details to the CRM when the user shares contact info."* 4. **Select Integration** * Choose your custom integration from the dropdown. * **Method & URL** will be pre-filled from the integration setup. 5. **Enable Request Components** * Based on your API method, you can **turn on/off**: * **Headers**: For sending additional information about your API request, such as authentication or metadata. * Example:`Key: Authorization Value: Bearer ` * **Params**: For adding filters or modifying the request. You can add multiple key-value pairs. * Example:`Key: age Value: 30 `will filter by age * **Body**: Data to send in the request (usually JSON). * Example: ```yaml theme={null} { "user_text": "{{user_text}}", "sender_id": "{{sender_id}}" } ``` * 6. **Set Timout** * Define the maximum wait time (in seconds) before the request times out. * If the server doesn’t respond in time, the request fails with a timeout error. 7. **Choose Trigger** * **Post-Call:** Executes after the conversation ends. * If **Post-Call** is selected, you can: * Add **Variables** (sent to the API). * **On-Call:** Executes during the conversation. * If **On-Call** is selected, you can: * Write a message in the **Speak During Action** section. * Add **Before API Call Variables** (sent to the API). * Add **After API Call Variables** (received from the API). *** ## **4. Test Your Custom API Action** 1. **Open Testing Mode** * Go to **Manage Agents → Select Agent → Test**. 2. **Start a Test Conversation** * Speak or chat with your agent. 3. **Trigger the Action** * Use the same instructions or keywords from your Action’s **Description** to activate it. 4. **Check API Logs** * In Gnani Agents, click **More Options (⋮)** on the Action card → **View Logs**. * Review request/response details and confirm the API executed correctly. 5. **Validate in External System** * If your API triggers a process (e.g., record creation), check the connected system to confirm results. *** **Your Custom API integration is now ready!** You can now expand your agent’s abilities to virtually any API-enabled service. # Integrating Gnani Agents with Webex Contact Center Source: https://docs.gnani.ai/D06_Webex Leverage the power of Gnani Agents seamlessly within your Cisco Contact Center workflows. Gnani Agents integrates with Webex Contact Center through Cisco’s **Service App** framework. Once set up, you can use Gnani Agents inside the Webex **Flow Designer** to handle customer interactions seamlessly. As an Admin, here’s what you need to do: ## Service App Creation: Admin Flow ### Step 1: Go to Service Apps in Control Hub * Sign in to your Webex **Control Hub**. * Navigate to **Management → Apps → Service Apps**. ### Step 2: Locate the Gnani Agents Service App * You will see **Agent Builder AI Agent** listed as a Service App available for your organization. * Click on it to review the details and scopes requested. ### Step 3: Authorize the Service App * Click **Authorize** to enable the Agent Builder Service App. * This grants the app the required permissions to function within your Webex environment. ### Step 4: Confirmation * Once authorized, the Gnani team will complete the backend setup. * You can now add Gnani Agents directly in the **Cisco Contact Center** **Flow Designer** to manage customer calls and chats. ## After Authorization * No further configuration is needed from your side. * The Gnani **team** will handle credit setup and ensure that your AI agents are active. If you face any issues or need more credits, contact us at [**hello-inya@gnani.site**](mailto:hello-inya@gnani.site). That’s it, with just a one-time authorization, your organization can start using Gnani Agents inside Webex Contact Center workflows.\ \ We'll assist you with: * Purchasing and allocating required credits * Completing backend setup and Webex deployment * Verifying that your Gnani Agents are active within the Contact Center flow designer ## To add the Gnani Agents Transcript widget to the Desktop Layout: 1. Login to Cisco Webex Contact Hub Desktop as an Administrator 2. Click on **Contact Center** under **Services** on left pane, and then go to **Desktop Layout** under **Desktop Experience** Web1 Pn Web2 Pn 3. From the desktop Layouts page that opens, select the layout you want to edit. 4. Click on the download button next to the already uploaded JSON file to download so that you can edit it. Web3 Pn 5. In the .json file, add the following details to create another tab for the Agent Builder Transcript Widget to show up under “agent” > “area” > “panel” > “children” array ```json theme={null} { "comp": "md-tab", "attributes": { "slot": "tab", "class": "widget-pane-tab" }, "children": [ { "comp": "slot", "textContent": "Agent Builder Transcript", "attributes": { "name": "INYA_TRANSCRIPT_TAB" } } ] }, { "comp": "md-tab-panel", "attributes": { "slot": "panel", "class": "widget-pane" }, "children": [ { "comp": "dynamic-area", "attributes": { "name": "INYA_TRANSCRIPT" }, "properties": { "area": { "id": "inya-transcript", "widgets": { "inya": { "comp": "inya-transcript", "script": "https://genvoice-appdev.gnani.site/sdk/inya/inya-webex-widget.min.js", "properties": { "agentId": "$STORE.agent.agentId", "taskMap": "$STORE.agentContact.taskMap", "agentName": "$STORE.agent.agentName", "darkMode": "$STORE.app.darkMode", "accessToken": "$STORE.auth.accessToken" }, "wrapper": { "title": "Agent Builder Transcript", "maximizeAreaName": "app-maximize-area" } } }, "layout": { "areas": [["inya"]], "size": { "cols": [1], "rows": [1] } } } } } ] } ``` 8. Once changes are done, save the file and upload the file in the Layout 9. Click **Save** ### Summary By integrating Gnani Agents **with Webex**, you get all the advantages of intelligent, conversational AI embedded straight into your Cisco Contact Center. Just: 1. Set up via the Webex App Hub 2. Create your agents within Gnani Agents 3. Contact us at [**hello-inya@gnani.site**](mailto:hello-inya@gnani.site) for credit setup and activation 4. Insert your agents into Webex’s flow designer to power customer conversations # Conversational Logs Source: https://docs.gnani.ai/E01_Conversational_Logs Your agent's flight recorder Want to replay a user conversation or debug a tricky call? **Conversational Logs** are your time machine! ### What Are Conversational Logs? Conversational Logs capture every conversation your agent has, including both test sessions and live interactions. They offer a detailed view of what happened during each call or chat, allowing you to review, analyze, and improve your agent’s performance. ### Why Are They Important? * **Insightful Analysis:** Understand the flow of each conversation. * **Performance Tracking:** Monitor call durations, latency, and overall performance. * **Troubleshooting:** Identify issues like delays or miscommunications. * **Quality Assurance:** Review call insights to assess resolutions and agent behavior. ### How to Access Conversational Logs You have several ways to view these logs: 1. **For All Agents:** * Go to **Agent Related → Conversational Logs** to see logs for all your agents together. 2. **For All Agent Chains:** * Navigate to **Agent Chains → Conversational Logs** for a consolidated view of your agent chains. 3. **For a Specific Agent:** * Go to **Agent Related → Manage Agents**, select an agent, then click **View Logs** at the top right. 4. **Post-Test Shortcut:** * After ending a test conversation, a snack bar appears with a link to view that specific conversation log. ### What You’ll See in a Conversation Log * **Start and End Times:** Know exactly when the conversation began and ended. * **Latency:** Track any delays incurred during the conversation. * **Call Insights:** Get summarized details on the call’s reason, result, overview, and resolution. * **Recording and Transcript:** Listen to or read the complete conversation for in‑depth analysis. # Dev Logs Source: https://docs.gnani.ai/E02_Dev_Logs Troubleshoot agent behavior ### **What Are Dev Logs?**  Dev Logs are a timestamped sequence of system-level events that occur during the lifecycle of a voice/chat interaction. These logs capture each stage of the agent orchestration, starting from the initial greeting to transcription, LLM processing, voice synthesis, and call disconnection. It's built for developers and advanced users who want visibility into every stage of the agent's thinking and speaking process.   ### **Where to Find It**  You can access Dev Logs by navigating to:
**Conversation Logs → Dev Logs**   ### **Why Use Dev Logs?**  Dev Logs are essential when:  * You want to debug why an agent didn't respond correctly  * You need to analyze latency at each stage (TTS, ASR, LLM)  * You're optimizing call performance  ### **Log Source / Module Tags**  Each log line is prefixed by a module to indicate where it originated:  | **Module** | **Description** | | :--------------- | :---------------------------------------------------------------------------------------------------------------------- | | CALL | Tracks call lifecycle: start, disconnect | | ORCH | Orchestration logic like greeting fetch, post-call actions, post-call processing | | TTS | Text-to-speech processing, TTFB (time to first byte), latencies, audio durations | | ASR / TRANSCRIBE | Speech recognition, transcription results, audio duration and latency | | LLM | Large Language Model events, streaming, token usage info, language switch detection, error handling if generation fails | | KB | Knowledge base context retrieval and metrics | | BARGE | User interruption (barge-in) detection and handling | | ERROR | Error responses, especially from OpenAI or other APIs | | UNKNOWN | Fallback logs for unexpected events or raw debug info | ### **Latency Monitoring**  Look out for high latency values in:  * **ASR Latency**  * **TTS Latency**  * **LLM Generation Time**  High values may indicate network, model, or transcription delays. These are key areas to optimize.  ### **End of Conversation Signals**  Look out for:  * EOC: End of Conversation  * CALL: Disconnecting call: User hang-up or timeout  * ORCH: Finalizing total call credits: Post-call processing  ### **Debugging Tips**  | **Issue** | **Check This Section** | | :---------------------------------------- | :--------------------- | | Agent responded slowly | TTS / LLM latency | | Agent misunderstood user | ASR final result | | Agent gave a wrong or incomplete response | LLM logs | | Response wasn’t spoken | TTS | | Unexpected disconnect | CALL logs at the end | ### **Summary**  The Dev Logs tab is your deep-dive tool for full transparency into:  * What your agent said  * What the user said  * How long each step took  * What the model generated and why  It’s invaluable for debugging, improving accuracy, and ensuring top performance.  # Agent Analytics Source: https://docs.gnani.ai/E03_Agent_Analytics Your Agent’s Report Card Is your agent chatty, efficient, or needs improvement? Find out here! ### What Are Agent Analytics? Agent Analytics provides an overview of your agent’s performance through key metrics and trends. It’s your go-to tool for tracking how your agent interacts with users over time. ### Key Metrics You Can Monitor * **Total Calls/Chats:** The overall number of interactions. * **Total Duration:** The combined duration of all calls and chats. * **Average Duration:** The average length of a conversation. * **Connected Calls:** Calls successfully connected with users. * **Sentiment Trend:** Insights into the emotional tone (positive, negative, neutral) of interactions over time. * **Call Insights:** * Top reasons: Reason of the call (e.g., refund requests) * Intents: User intents identified in conversations. * Topics: Frequently mentioned keywords (e.g., shipping delays) * Dispositions: Call outcomes (e.g., resolved, unresolved, follow up requested) * **Total Actions Triggered:** The number of integration actions executed. * **Call Drop-off Rate:** The percentage of calls where users disconnected early. ### How to Access Agent Analytics 1. Navigate to **Agent Related → Manage Agents**. 2. Select the agent you want to analyze. 3. Click on **Analytics** at the top right. ### Why This Is Valuable These analytics help you understand not just what your agent is doing, but how well it’s performing. Use these insights to fine-tune prompts, adjust integrations, and enhance the overall user experience. # Action Logs Source: https://docs.gnani.ai/E04_Action_Logs Your Integrations’ Detective tool. *"Did My SMS Send? Why Did the CRM Update Fail?" Action Logs are where you’ll find your answers.* ### What Are Action Logs? Action Logs help you verify that the actions triggering integrations with external services (CRM, email, SMS, etc.), are working correctly. They provide detailed records of every action executed during conversations. ### Why Use Action Logs? * **Functionality Check:** Confirm that your integrations are performing as expected. * **Detailed Feedback:** View trigger time, payload data, response, and status codes. * **Troubleshooting:** Quickly pinpoint and resolve any issues with action execution. ### How to Access Action Logs 1. Navigate to **Agent Related → Manage Agents**. 2. Select the specific agent you want to review. 3. Go to the **Action** tab. 4. Click the kebab menu (three vertical dots) for the action you’re interested in. 5. Select **View Logs** to see the triggered actions list. Screenshot 2025-08-11 at 2.55.07 PM.png ### What to Look For * **Triggered Time:** When the action was initiated. * **Payload:** The data sent to the external service. * **Response:** Feedback from the service. * **Status Code:** Code indicating success or type of error. * **Parameters:** Additional details or variables used in the action. # Need Help? Source: https://docs.gnani.ai/F01_Support Get Help, Share Feedback, and Report Issues The **Support Desk** allows you to quickly raise tickets for bug reports, feature requests, account issues, and general feedback. Whether you need technical assistance or want to suggest an improvement, this section ensures your concerns are addressed efficiently. *** ## How to Raise a Support Ticket ### Step 1: Open the Support Desk * Navigate to the **Support Desk** option in the sidebar. * Click to open the support ticket submission form. ### Step 2: Fill in Ticket Details The form contains five key fields to provide relevant information: 1. **Raising Ticket As:** * Your registered email is auto-filled in this section. * This field is non-editable, ensuring all tickets are linked to the logged-in user. 2. **Ticket Title:** * Enter a short, clear title describing the issue or request. 3. **Category:** * Choose from the following categories: * **Bug Report:** Reporting errors, glitches, or unexpected behavior. * **Feature Request:** Suggesting a new feature or improvement. * **Account Issue:** Problems related to login, billing, or account settings. * **Feedback:** General comments or suggestions. * **Others:** Anything that doesn’t fit the above categories. 4. **Description:** * Provide a detailed explanation of the issue or request. * Include steps to reproduce the issue (if applicable) and any relevant details. 5. **Upload Screenshots (Optional):** * Click to upload or drag and drop up to **three** image files. * Supported formats: **JPG, JPEG, PNG** (Max size: **5MB per file**). ### Step 3: Submit the Ticket * Once all details are filled in, click **Submit**. * Your ticket will be logged, and the support team will review it promptly. *** ## Best Practices for Raising a Ticket * **Be specific:** Provide clear and concise information. * **Attach screenshots:** Helps the support team understand the issue faster. * **Use relevant categories:** Ensures your ticket is handled efficiently. * **Check the help docs:** Look for solutions before submitting. Once submitted, you can expect timely updates on your ticket's progress. Need urgent help? Look out for additional support channels within the platform. **Your feedback helps us improve — don't hesitate to reach out.** # Organizations Source: https://docs.gnani.ai/F02_Org This guide explains how Organizations, Roles, Agent Access, Sharing, and Environments work in Gnani Agents. It is designed for teams that want structured collaboration, controlled access, and a safe promotion workflow before going live. *** ## Who Should Use Organizations? Organizations are ideal for: * Teams building agents collaboratively * Companies separating development and testing responsibilities * Businesses that need controlled access to agents * Teams that want structured Development → Staging → Production workflows If you are working alone, you may use your Personal workspace. If you are working with a team, using an Organization is strongly recommended. *** ## Why Use an Organization? Using an Organization provides: * Clear role-based access (Developer, QA, Org Admin) * Controlled agent visibility * Structured testing before production readiness * Centralized ownership and oversight * Safe collaboration without accidental overwrites Organizations ensure that only the right people can build, test, and prepare agents for deployment. *** ## 1. Understanding Organizations ### What is an Organization? An Organization is a shared workspace where: * Team members collaborate * Agents are created and managed * Agents move through Development → Staging → Production Deployment to live infrastructure is handled by the Gnani Agents team. Agents must be in the Production environment before deployment can be requested. ### How to Create an Organization Organizations are not created through the UI. If you need an Organization set up, please contact us and we will create it for you. We will also assign the initial Org Admin from our end. If you need additional Org Admins added later, you can contact us for assistance. *** ## 2. Roles An Organization includes three user roles: * Org Admin * Developer * QA Permissions are currently fixed. Custom roles and granular permission editing are coming soon. | Role | Primary Responsibility | | :-------- | :----------------------------------------- | | Org Admin | Manage team members and oversee all agents | | Developer | Build and manage agents in Development | | QA | Test agents in Staging and mark them ready | *** ## 3. Role Responsibilities ### Org Admin The Org Admin oversees the organization. Can: * Add/Remove Developers and QA users * Change user roles (Developer ↔ QA) * View and edit all agents * Promote agents through environments ### Developer Developers are responsible for building agents. Can: * Create agents in Development * Edit agents in Development * Share agents with other Developers * Promote agents to Staging Cannot: * Edit agents in Staging * Edit agents in Production * Deploy agents ### QA QA users validate agents before production readiness. Can: * View agents in Staging * Test agents * Mark agents as "Ready for Production" Cannot: * Edit agents * Deploy agents * Access Development unless granted visibility through the environment workflow *** ## 4. Membership Concept (Agent Access) Agent access works on a membership model. You can think of this as: * Agent Access List * Agent Membership * Agent Ownership & Sharing When a Developer creates an agent: * That Developer automatically has membership * The Org Admin automatically has membership No one else can see or access that agent unless it is explicitly shared. This ensures agents are private by default and only visible to intended collaborators. ### Sharing Agents Agents can be shared with other Developers within the Organization. When sharing an agent, there are two access levels: * Read-Only Access * Edit Access ### Edit Access Rules * Only the **bot owner** (the Developer who created the agent) can grant **edit access** to another user. * A user who is **not the bot owner** can only share the agent as **read-only**. This ensures ownership control while still enabling collaboration. Once shared: * Read-only users can view the agent configuration but cannot modify it. * Users with edit access can make changes in the Development environment. QA users do not receive access through manual sharing.\ QA visibility is granted automatically when the agent is promoted to Staging. ### Deletion Rules for Shared Agents If the agent creator (Developer A) shares an agent with another Developer (Developer B): * If Developer B deletes the agent from their view, only their **membership** is removed. * The agent itself is **not deleted** from the Organization. * The agent remains visible to the bot owner, Org Admin, and any other users who have access. Only the bot owner can permanently delete the agent from the Organization. *** ## 5. Agent Visibility Rules ### Default Visibility When a Developer joins an Organization: They see no agents by default. They only see: * Agents they created * Agents shared with them ### Org Admin Visibility Org Admins have visibility into all agents within the organization. ### QA Visibility QA users: * Do not see Development agents * Automatically gain visibility when an agent is promoted to Staging This ensures structured separation between building and testing. *** ## 6. Personal vs Organization Workspace Every user has a Personal workspace. Organization workspaces are shared team environments. Currently: * Transfer of agents from Personal → Organization is not available * Agents cannot be moved between organizations Agent transfer capabilities are coming soon. Agents must be created directly within the intended workspace. *** ## 7. Environments & Workflow The workflow follows a strict linear structure: Development → Staging → Production There is currently: * No version history * No rollback * No skipping environments * Overwrite model (each promotion replaces the previous configuration) Versioning and advanced release management capabilities are coming soon. ### Development Access: * Developer * Org Admin Purpose: * Build and edit the agent * Modify prompts, flows, configurations Action Available: * Promote to Staging Effect: * Development configuration is copied to Staging * Any existing Staging version is overwritten ### Staging Access: * QA (full testing) * Org Admin (testing) * Developer (read-only) Purpose: * Validate agent behavior * Conduct testing Action Available (QA): * Mark "Ready for Production" No editing is allowed in Staging. ### Production Access: * Org Admin (view and manage status) * Other users (view only) Production represents the finalized configuration. Deployment is handled by the Gnani Agents team. An agent must be in Production before deployment can be requested. *** ## 8. Deployment Process Deployment to live infrastructure is not triggered directly from the platform. Once: * The agent is in Production * Testing is complete You can contact the Gnani Agents team to initiate deployment. Only agents in Production are eligible for deployment. *** ## 9. Integration Deletion Safety If a Developer removes an integration from their agent: * It is removed from their access * It remains in the system if another agent is using it An integration cannot be permanently deleted if it is linked to any active agent. This prevents accidental disruption of other team members’ agents. *** ## 10. Current Limitations The following capabilities are not yet available but are coming soon: * Audit logs * Version history and rollback * Self-serve organization creation *** ## Summary Organizations provide: * Structured team collaboration * Clear role separation * Controlled agent visibility * Safe Development → Staging → Production workflow * Managed deployment process through the Gnani Agents team This ensures predictable collaboration, clean testing flows, and controlled production readiness. # Environments Source: https://docs.gnani.ai/F03_Env Environments allow teams to build, test, and prepare agents for deployment in a structured and controlled manner. This feature is available only within Organizations and is designed to separate development, validation, and production readiness. *** ## Overview Each agent inside an Organization has three environments: * Development * Staging * Production These environments follow a strict linear workflow: Development → Staging → Production Important: * There is no version history. * Saving changes overwrites the current state of that environment. * Promoting to the next environment overwrites the configuration in that target environment. Versioning and rollback capabilities are coming soon. *** ## 1. Development Environment ### Who Has Access? * Developer * Org Admin ### What Can Be Done Here? Full editing capabilities are available: * Modify system prompts * Edit conversation flows * Update knowledge base * Change configurations * Adjust integrations This is the primary workspace where agents are built and iterated. ### Action: Promote to Staging When you click **"Promote to Staging"**: * The current Development configuration is copied to Staging. * Any existing Staging configuration is overwritten. * QA users automatically gain visibility of the agent in Staging. This ensures QA only sees agents that are ready for testing. *** ## 2. Staging Environment Staging is used for validation and testing before moving to Production. ### Who Has Access? * QA * Org Admin ### What Can Be Done Here? QA can: * Test conversations * View logs * Validate behavior Editing is not allowed in Staging. ### Action: Mark "Ready for Production" Available to: QA When QA clicks **"Mark Ready for Production"**: * The agent status changes to **Ready for Production**. This signals that the agent has passed testing and is ready for final approval. *** ## 3. Production Environment Production represents the finalized configuration of the agent. ### Who Has Access? * Org Admin ### What Can Be Done Here? * The agent handles live traffic once deployed. * Editing is not allowed in Production. ### Action: Push to Production Available to: Org Admin only When **"Push to Production"** is clicked: * The Staging configuration is copied to Production. * Any existing Production configuration is overwritten. This ensures that only a QA-approved configuration reaches Production. *** ## How Environment Promotion Works Each promotion step fully replaces the target environment. For example: * Promoting from Development to Staging replaces everything in Staging. * Pushing from Staging to Production replaces everything in Production. Because there is no version history, previous configurations cannot be restored automatically. *** ## Deployment Production represents the final approved configuration. Deployment to live infrastructure is handled by the Gnani Agents team. Only agents that are in the Production environment are eligible for deployment. *** ## Best Practices * Complete all development work before promoting to Staging. * Ensure QA thoroughly tests before marking Ready for Production. * Confirm readiness before pushing to Production, since the action overwrites the existing configuration. *** ## Summary The Environments feature ensures: * Clear separation between building and testing * Controlled approval before Production * Reduced risk of accidental live changes * Structured collaboration within Organizations This workflow helps teams maintain quality, control, and clarity as agents move toward deployment. # Setting Up Integrations Source: https://docs.gnani.ai/M03_Integrations Connect your agent to SMS, email, CRM, ticketing systems, and third-party APIs. ## **Introduction** Integrations allow your GenAI agent to connect with external systems, automating workflows and enhancing functionality. With integrations, your agent can **send emails, SMS messages, update CRMs, and manage tickets** effortlessly. To use these integrations, you’ll need to configure **Actions**, which will be covered in the next module. **To add an integration:** Navigate to **Manage Agents** → Select Agent → **Integration** tab → Add integration and select the required integration type. *** ## **Lesson 1: SMS Integrations** ### **Why Integrate SMS?** Enable your agent to send appointment reminders, ticket confirmations, discount codes, and more via SMS. ### How to Set Up: 1. In the Add SMS Integration section, select Twilio as the operator, and provide a name for your integration. 2. Open the **Twilio Console Dashboard** ([https://console.twilio.com/dashboard](https://console.twilio.com/dashboard)) to retrieve credentials. 3. Copy the **Account SID** and **Auth Token** from the Account Info section and paste them into the platform. * **Account SID:** A unique 34-character alphanumeric identifier starting with "AC". * **Auth Token:** A secure key for authentication. 4. Choose the Sender type: * **Phone Number:** Send messages from a fixed number for consistent branding. * In Twilio, go to **Develop → Phone Numbers → Manage → Active Numbers**, select an active SMS-enabled number, and paste it into the platform. * If you don’t have a number, click **Buy a Number** in Twilio. * **Messaging Service:** Use a pool of numbers for sending SMS dynamically based on factors like location and compliance. * In Twilio, navigate to **Develop → Messaging → Services**, find the **Messaging Service SID** (starting with "MG"), and paste it into the platform. * If you don’t have a messaging service, click **Create Messaging Service** in Twilio. 5. Click **Save** to complete the integration. *** ## **Lesson 2: Email Integrations** ### **Why Integrate Email?** Enable your agent to send **welcome emails, follow-ups, newsletters, and transactional messages** automatically. ### **How to Set Up:** 1. In the **Add Email Integration** section, select **Mailchimp** as the operator and provide an integration name. 2. Enter the **Mailchimp API Key** (found in your Mailchimp account). 3. Click **Save** to finalize the integration. *** ## **Lesson 3: CRM Integrations** ### **Why Integrate CRM?** Allow your agent to **automatically create customer records** in Zoho CRM, streamlining lead and support management. ### **How to Set Up:** 1. In the **Add CRM Integration** section, select **Zoho** as the operator and enter an integration name. 2. Open the [Zoho API Console](https://api-console.zoho.in/) to retrieve credentials. 3. Follow [Zoho's guide](https://www.zoho.com/accounts/protocol/oauth/self-client/overview.html) to create a **Self Client**. 4. In the **Client Secret** tab, find the **Client ID** and **Client Secret** and paste them into the platform. 5. Enter the **Account Server URL** based on your primary user region ([choose from here](https://www.zoho.com/accounts/protocol/oauth/multi-dc.html)). 6. Generate the **Account SOID** using step 1 in [this guide](https://www.zoho.com/accounts/protocol/oauth/self-client/authorization-code-flow.html). 7. Specify the required scopes from [Zoho's scope list](https://www.zoho.com/crm/developer/docs/api/v3/scopes.html). 8. Click **Save** to complete the integration. *** ## **Lesson 4: Ticketing Integrations** ### **Why Integrate Ticketing?** Enable your agent to **create support tickets** in Zoho Desk, automating customer service processes. ### **How to Set Up:** 1. In the **Add Ticket Integration** section, select **Zoho** as the operator and enter an integration name. 2. Open the [Zoho API Console](https://api-console.zoho.in/). 3. Follow [Zoho's guide](https://www.zoho.com/accounts/protocol/oauth/self-client/overview.html) to create a **Self Client**. 4. Retrieve the **Client ID** and **Client Secret** from the **Client Secret** tab and paste them into the platform. 5. Enter the **Account Server URL** ([choose from here](https://www.zoho.com/accounts/protocol/oauth/multi-dc.html)). 6. Generate the **Account SOID** using [this guide](https://www.zoho.com/accounts/protocol/oauth/self-client/authorization-code-flow.html). 7. Specify the required scopes from [Zoho Desk's scope list](https://desk.zoho.com/DeskAPIDocument#OauthTokens#OAuthScopes). 8. Click **Save** to finalize the integration. *** ## **Lesson 5: Custom Integrations** ### **Why Use Custom Integrations?** For unique use cases, integrate **custom APIs** to expand your agent’s capabilities beyond built-in options. ### **How to Set Up:** 1. In the **Add Custom Integration** section, enter a name and description. 2. Choose an **API Call Method** (GET, POST, etc.) and enter the **API URL**. 3. If authentication is required, enter the **Key** and **Value**. 4. Click **Integrate** to complete the setup. *** Your agent is now integrated and ready to automate workflows. # Managing Variables & Actions Source: https://docs.gnani.ai/M04_Actions Create variables and actions to automate workflows and handle dynamic conversation data. ## **Introduction & Context** Variables and actions power automation in your genAI agent. **Variables** allow dynamic data handling, while **actions** trigger workflows based on user interactions. *** ## **Lesson 1: Understanding Variables** ### **What Are Variables?** Variables act as placeholders for storing and managing data throughout a conversation. They can: * **Capture user input** (e.g., name, phone number, order details). * **Store API responses** (e.g., retrieving order status). *(Coming Soon)* * **Pass data into API requests** (e.g., including an ID for fetching details). ### **How to Create a Variable?** 1. **Define a Name** – Use letters, numbers, or underscores (e.g., `user_name`, `order_id`). 2. **Choose a Data Type**: * **Boolean** (True/False) * **Integer** (Whole numbers) * **Float** (Decimal numbers) * **String** (Text, such as names or messages) 3. **Set a Prompt** – Define how the variable gets its value. * Example: "Ask the user for their PIN code (must be 6 digits)." ### **When to Use Variables?** * **On-Call Triggering:** * **Before an API Call** – Store user input to pass into an API request. * **After an API Call** – Save data received from an API response. *(Clarification needed: What happens if the data isn’t stored?)* * **Post-Call Triggering:** Use the variable to execute actions after a conversation. ### **Use Cases** Personalizing emails, customizing SMS messages, or logging details in CRM systems. *** ## **Lesson 2: Setting Up Actions** ### **What Are Actions?** Actions define **triggers** that send automated responses or execute integrations based on specific conditions. ### **How to Create an Action?** 1. Go to **Manage Agents → Select Agent → Action Tab**. 2. Click the **"+"** button to open the "**Create Action**" card. 3. **Enter a Name** - Use only letters, numbers, or underscores. 4. **Describe the Action** – Example: "Trigger this action when the user asks about available offers." 5. **Select an Integration** – Choose the service to execute the action. 6. **Choose a Trigger:** * **Post-Call** – Executes after the conversation ends. * **On-Call** – Executes during the conversation. 7. **Create or Add Variables (Optional):** * Use variables to store dynamic data like customer names or order details. * You can create new variables or reuse existing ones. ### **Integration-Specific Fields** The fields below depend on the type of integration used. ### **For SMS Action:** * **Message Template:** Define the SMS content. Insert variables using curly braces: * Example: `"Hello {{user_name}}, your order {{order_id}} is on the way!"` ### For Email Action: * **Select Action:** *(Work in Progress)* * **Template Name:** Select an email template from Mailchimp. * **From Name & Email:** The sender’s verified email address in Mailchimp. * **To Name & Email:** The recipient's email (variable input is recommended). * **Add Subject:** Customizable subject line (can include variables). * **Merge Variables:** Map platform variables to Mailchimp template variables: * **Key:** Variable in Mailchimp template. * **Value:** Corresponding variable created in Agent Builder (use curly braces). *** ### For CRM Action: * **Select Action:** Choose a CRM action (e.g., CREATE for adding new records). *(Work in Progress)* * **Module Name:** Specify the CRM module (e.g., Leads, Accounts, Contacts, Deals). * **Payload:** Define JSON data format:\{ "Company": "Example Corp", "name": "Smith", "Email": "[john.smith@example.com](mailto:john.smith@example.com)"} ### For Ticket Action: * **Select Action:** Choose a ticketing action (e.g., CREATE for new support tickets). *(Work in Progress)* * **Department ID:** Specify the Zoho Desk department where the ticket should be created. * **Contact ID:** Associate the ticket with a customer profile. Currently this is for testing the capabilities of the platform, but it will be a dynamic field in the future. * **Payload:** Define the ticket structure in JSON format. The subject, description and priority are mandatory fields to create a ticket in Zoho Desk. \{ "subject": "\{\{subject}}", "description": "\{\{description}}", "priority": "\{\{priority}}" } ### For Custom API Action: * **Method & URL:** Pre-populated based on the integration. * Options to turn headers, params and body on and off will be available based on the API Method. * **Headers:** Send additional information about your API request, such as authentication details or metadata: * Example: * **Key:** `Authorization` * **Value:** `Bearer ` * **Params:** Params help filter or modify the request. You can add multiple key-value pairs. * Example: Filter by age: * **Key:** `age` * **Value:** `30` * **Body:** The body contains the data sent to the API, which is usually in JSON format. You can add multiple key-value pairs. * **Key**: `name` → **Value**: `Jack Reacher` * **Key**: `email` → **Value**: `reacher@example.com` * **Timeout:** Define max wait time before the request times out. The platform will wait for a response from the server for the defined time before canceling the request. If the server doesn’t respond within this time, the request fails with a timeout error. \*Your genAI agent is now equipped with powerful automation tools! \* ## **Lesson 2: Testing your Actions** # Agent Chaining Source: https://docs.gnani.ai/M05_Chains Build multi-step conversational flows that adapt to user needs with agent chaining. The Agent Chaining feature on the Agent Builder platform empowers you to build sophisticated, multi-step conversational workflows that truly understands and adapts to your users’ needs. With this, you can design a dynamic conversation pipeline where various nodes interact, decide, and make intelligent decisions. This guide will help you craft your own interactive flow! ## Lesson 1: What is Agent Chaining? **Agent Chaining** is a visual and intuitive way to design conversation flows. Imagine it as a flowchart where each box (or node) represents a different part of the conversation. By connecting these nodes on a freeform canvas, you create a structured flow where each node handles a specific action or decision point. ### Why build an Agent Chain? Imagine your genAI agent as a chef: a single recipe (prompt) makes one dish, but **Agent Chaining** lets you create a *full menu* with appetizers, mains, and desserts! Similarly, Agent Chaining transforms a single specialized agent into a full-scale organization, with genAI ‘employees’ working across departments to solve complex business challenges. A single-prompt conversational agent may struggle with complex, multi-step interactions. A large system prompt can cause it to lose track of events. With Agent Chaining, you overcome these limitations and gain: * **Greater Control:** Guide your agent’s conversations through a predefined flow. * **Contextual Decisions:** Decision nodes enable responses based on user input and conditions. * **Enhanced User Experience:** Each node specializes in a purpose, making interactions smoother, natural and accurate. * **Specialization:** Different knowledge bases or actions can be assigned to specific steps. * **Avoid Pitfalls:** Prevent your agent from looping and hallucination by defining clear pathways. **Use Cases:** * **Customer Support:** The agent follows structured troubleshooting steps and escalates to a human agent when needed. * **Sales:** Qualify leads with conditional questions and transfer calls when all criteria are met. * **Business Owners:** Create customized conversation flows for various industries like healthcare, legal, and finance, ensuring every type of query is handled accurately. *** ## Lesson 2: Understanding the Node Types ### What Are Nodes? Nodes are building blocks of your conversation flow. Each node represents an independent agent, by linking them together and defining conditions, you design the conversation path your agent chain will follow. ### Node Types Explained 1. **Default Node** * **Purpose:** You can define the purpose of this node to your liking. It handles general queries and serves as a fallback when no other node fits. * **How to Use:** * Define a clear prompt for the agent’s response. * Enable the “Static Text” if you want a fixed response. * **Tip:** Use detailed context-rich prompts to ensure natural, effective responses. 2. **User Node** * **Purpose:** Captures input from the user. * **How to Use:** Simply add a User node to your flow where you need to collect information like a name, email, or specific responses. 3. **Decision Node** * **Purpose:** Determines the next step in the conversation based on set conditions. * **How to Use:** * **Decision Prompt:** Add a condition to choose that path. (E.g., User’s age is less than 18) * **Condition:** Optionally, define a prompt in the decision node for complex conditions. * **Repeat Count:** Toggle and set a limit for allowing reattempts. * **Knowledge Base:** Link a knowledge base for contextual decision-making. 4. **Event Node** * **Purpose:** Triggers key events like ending the call, transferring the call to a human agent, or resetting the flow. * **How to Use:** Select an event type (such as End Call, Reset, or Transfer) and define the response type (dynamic prompt or a fixed message). **Connection Rules:** * **Default** → Can connect to **User** nodes. * **User** → Can connect to **Decision**, **Default**, or **Event** nodes. * **Decision** → Can connect to **Default** or **Event** nodes. * **Event** → Dead-end (conversation stops, restarts or gets transferred). *** ## Lesson 3: **Creating Your First Agent Chain** Now that you understand the basics, it’s time to create your very first agent chain. You can create an agent chain in three ways: from scratch, using a prompt, or uploading a JSON file. ### Lesson 3a: Creating from Scratch 1. **Start Fresh:** * Go to ‘**Agent Chains**’ and click ‘**+**’. * Select ‘**Create from Scratch**’, name your chain, choose an industry, and pick an icon. 2. **Begin with the Start node:** * A Start and User node are automatically created. * Customize the start node with a greeting message (static or prompt explaining how to start the conversation). 3. **Build Your Flow:** * Add more nodes as required by clicking on the ‘**Add Node**’ option at the bottom. * **Quick Add:** Hover over an existing node to reveal the connection points on the sides. Click on one to see a menu of compatible nodes that can be added and automatically connected to your selected node. * For default and event nodes, choose if the node should speak a static text or function dynamically according to the prompt. * For decision nodes, set up your conditions in the path and also the decision node. * Connect the nodes by dragging from one connection point to the next. 4. **Save and Test:** * Click ‘**Save**’ and try out your new flow to see how it works. *** ### Lesson 3b: Creating Using a Prompt 1. **Start Fresh:** * Navigate to ‘**Agent Chains**’ and click ‘**+**’. * Select ‘**Create from Prompt**’, name your chain, choose an industry, and pick an icon. 2. **Generate Your Chain:** * Provide a detailed prompt describing your conversation flow. * The system generates an initial flow based on your input. 3. **Review and Customize:** * Edit nodes, change connections, and refine the prompts to better fit your vision. 4. **Save and Test:** * Once you’re happy with the flow, save your work and run a test. *** ### Lesson 3c: Uploading a JSON File 1. **Prepare Your File:** * Ensure your agent chain is saved in a JSON file. You can find an example here. 2. **Import Your Flow:** * Create a new chain from scratch. * Click ‘**Import**’ in the top right corner, paste your JSON content and click on ‘**Import’**. 3. **Review the Imported Flow:** * The system will load your chain onto the canvas. * Adjust node configurations and layout if needed. 4. **Save Your Chain:** * Finalize and save your agent chain. *** ## Lesson 4: Configuring your Agent Chain ### Navigating the Canvas The canvas is your workspace for designing conversations. Think of it as a whiteboard where you visually map out each step of your conversation. * **Drag & Drop:** Move nodes by dragging them anywhere on the canvas. * **Connect Nodes**: Hover over a node to show connection points. Click and drag from a point to draw a line, then drop onto another node's connection point to create a flow. * **Zoom & Pan:** Pan by clicking and dragging anywhere on the canvas. To zoom, pinch using a trackpad or scroll using a mouse. * **Organization Tools: K**eep your chain neat and easy to manage by using the tools available in the bottom left. * ‘**Fit View**’ centers the flow and maximizes zoom out. * ‘**Auto Layout’** arranges the nodes neatly in a top-down format. * **Undo/redo** in the bottom bar helps manage changes. ### Configuring your Chains * **Configure:** Link global knowledge bases, customize agent languages, time zones, LLM, transcriber, TTS, agent phone numbers and transfer number (if a transfer event node is set up). * **Global** **Prompt:** Define common agent instructions (tone, style, etc). * **Manage Integrations:** Connect to third-party services. * **Manage Actions: C**reate actions to enable the agent to use the integrations. The actions can then be linked to specific nodes. # Dynamic Variables & Prompt Validation Source: https://docs.gnani.ai/M10_Dynamic_Variables Dynamic variables inject user-specific context into the agent's initial message and system prompt at runtime. Prompt validation checks that Jinja templates are syntactically correct before you save or test an agent. ### Path **Manage Agent → Customize tab → Dynamic Variables** ### What Dynamic Variables Do Dynamic Variables allow your AI agent to personalize its **Initial Message** and \*\*System Prompt \*\*using **runtime data** fetched from an external API. They replace placeholder text like: ```text theme={null} {{user_name}} {{plan_type}} {{ticket_status}} ``` …with real values like: * Rahul * premium * pending All **before** the request is sent to the LLM. ### Why Use Dynamic Variables? * Personalizes the agent’s tone, message, and instructions * Ensures responses are relevant to the **current user context** * Allows the same agent to adapt dynamically for different users and use cases * Integrates with your **backend systems** for context-aware conversations ### Where They’re Used Dynamic Variables can be embedded in: * **Initial Message** – The very first thing the agent says * **System Prompt** – The underlying instruction set that guides the LLM ### Placeholder Syntax * Always wrap variable names in **double curly brackets** * Use lowercase with underscores for clarity **Examples:** ```markdown expandable theme={null} {{user_name}} {{account_balance}} {{ticket_status}} ``` ### How It Works 1. **Agent Invoked** User starts a chat or API-triggered interaction. 2. **Variables Detected** The system scans templates for `{{variable_name}}` placeholders. 3. **API Call Made** If variables exist, the agent calls your configured **Dynamic Message API**. 4. **Values Returned** API responds with a JSON object, e.g.: ```json theme={null} { "user_name": "Rahul", "plan_type": "premium", "ticket_status": "pending" } ``` 5. **Template Compiled** Placeholders are replaced with actual values. 6. **LLM Receives Context** The prompt sent to the LLM is fully contextualized. ### Example System Prompt Template: ``` You are a support agent assisting {{user_name}}, a {{plan_type}} customer. Their last ticket was marked {{ticket_status}}. ``` API Response: ``` { "user_name": "Rahul", "plan_type": "premium", "ticket_status": "pending" } ``` Final Prompt Sent to LLM: ``` You are a support agent assisting Rahul, a premium customer. Their last ticket was marked pending. ``` ### Best Practices | Area | Recommendation | | ----------------- | ------------------------------------------------ | | Variable Naming | Use clear, lowercase, underscore-separated names | | API Reliability | Ensure low-latency, high-uptime APIs | | Defaults | Define fallback values for missing variables | | Message Integrity | Keep variables simple — avoid complex objects | | Testing | Test with real data before going live | ## Related feature: Prompt validation Your system prompt is treated as a Jinja2 template. That supports conditionals and loops, but requires validation before you save or test an agent. ### What validation checks * Syntax correctness for variables, `if` blocks, and `for` loops * Undeclared variables referenced in the prompt * Proper block closures (every opening tag has a matching closing tag) ### Invalid syntax behavior If syntax errors are found: * Save and Test are disabled * A clear error message is shown Common errors: ```text theme={null} Missing braces: {{ user_name } Unclosed block: {% if condition %} ... (missing {% endif %}) ``` ### Validation rules | Condition | Save / Test | Message | | ---------------------------- | ---------------- | ------------------------ | | Valid syntax | Yes | – | | Syntax error | No | "Invalid Jinja syntax." | | Missing variables at runtime | Yes (save works) | "Call triggered failed." | ### Agent behavior * Existing agents keep working until the system prompt is edited and saved again. * New and updated agents must follow Jinja formatting rules. ### Valid example ```text theme={null} Hello {{ customer_name }}, your booking is on {{ booking_date }}. ``` ### Invalid example ```text theme={null} Hello {{ customer_name } ``` Unclosed curly braces disable Save and Test until corrected. ### Tip If variables appear in the system prompt, declare them in **pre-call variables** or return them from the **Dynamic Message API**. Otherwise calls may fail at runtime. ### Summary | Field | Value | | ---------------- | ---------------------------------------------- | | Feature | Runtime variable injection | | Syntax | `variable_name` with double curly braces | | Input source | External API (JSON) | | Supports | Initial message, system prompt, ending message | | Resolved at | Runtime, before the LLM call | | Failure handling | Graceful fallback | # Import Twilio number Source: https://docs.gnani.ai/M16x_Import_Number Import phone numbers from Twilio and assign them to your voice agents. ## Importing a number from Twilio ### What Does Importing Mean? If you already own purchased phone numbers from Twilio, you can seamlessly import them into the Agent Builder platform. This saves time and allows you to use your existing numbers for both testing and production calls. ### Why Import Your Twilio Numbers? * **Efficiency:** Reuse your existing numbers without the hassle of buying new ones. * **Central Management:** View all your active numbers in one place. * **Enhanced Functionality:** Easily assign numbers to agents for inbound or outbound calls. ### How to Import a Twilio Number 1. **Access Active Numbers:** * Click on the **Phone Numbers** option in the sidebar. * Expand the section to show **Active Numbers**. 2. **Initiate the Import Process:** * If no numbers have been imported, you’ll see an option to **Import.** * If numbers are already present, the **Import** button will be available in the top right. 3. **Enter Your Credentials:** * You’ll be prompted to enter your Twilio **Account SID** and **Auth Token**. * The system validates these credentials. * If incorrect, an error message will prompt you to re-enter the correct details. 4. **Select and Import Numbers:** * Upon successful validation, a paginated table will display available Twilio numbers (excluding previously imported ones). * You can select one or multiple numbers (using checkboxes and a “Select All” option). * Confirm your selection, optionally add labels, and complete the import *Best Practice:* Double-check the list before confirming. If any number fails to import due to duplicates or API issues, the system will notify you of the ones needing attention. # Dashboard Source: https://docs.gnani.ai/M21_Dashboard The Big Picture The **Dashboard** is your mission control, offering a high-level view of your agents and chains. ### What’s on the Dashboard? * **Total Calls:** The overall number of calls or chats across your account. * **Call Duration:** Average conversation length for each agent and across all agents. * **Dialed vs. Connected Calls:** A comparison of calls made versus those successfully connected. Screenshot 2025-08-11 at 2.48.58 PM.png ### How to Access the Dashboard * Navigate to the **Dashboard** from the sidebar. This centralized view aggregates data from all agents and chains. ### Why Use the Dashboard? * **Quick Overview:** Get immediate insights into your overall performance. * **Performance Trends:** Monitor trends over time to spot improvements or areas for adjustment. * **Strategic Decisions:** Use data insights to scale and optimize agent interactions. *** # Add FAQ Entries Source: https://docs.gnani.ai/Platform/Add_FAQs POST /v1/agents/{botId}/faqs Add one or more FAQ entries to an agent's knowledge base **What it does:** Add FAQ entries to a voice agent's knowledge base so the agent can recognize caller questions and respond with predefined answers. A successful request automatically sets `faqEnabled = true` on the agent. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Body | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------------------- | | `botId` | string | Yes | Must match the `botId` path parameter. | | `environment` | string | Yes | Target environment: `development`, `staging`, or `production`. | | `faq` | array | Yes | Array of FAQ entry objects to add. | ### FAQ Entry Object | Field | Type | Required | Description | | ----------- | --------- | -------- | --------------------------------------------------------------------------------------- | | `questions` | string\[] | Yes | One or more question phrasings that map to this answer. Maximum 10 questions per entry. | | `answer` | string | Yes | The answer the agent will speak when a matching question is detected. | | `language` | string | Yes | BCP-47 language code for this FAQ entry (e.g. `en-IN`, `hi-IN`). | | `signal` | string | No | Optional label used for downstream analytics or post-call triggers. | ## Minimum working example ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/faqs" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botId": "fb79920229d144608ebf665a10e50275", "environment": "development", "faq": [ { "questions": [ "What are your business hours?", "When are you open?", "What time do you close?" ], "answer": "We are open Monday to Friday, 9am to 6pm IST.", "language": "en-IN" }, { "questions": [ "How do I cancel my subscription?", "I want to cancel" ], "answer": "To cancel your subscription, please visit our website or contact our billing team at billing@example.com.", "language": "en-IN", "signal": "CANCELLATION_INTENT" } ] }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Successfully created 2 FAQ(s)", "response": { "createdCount": 2, "faqIds": ["faq_xyz001", "faq_xyz002"], "totalFaqs": 5 } } ``` ## Response Fields | Field | Type | Description | | -------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `createdCount` | integer | Number of FAQ entries successfully created in this request. | | `faqIds` | string\[] | Array of identifiers for the newly created FAQ entries. Store these to update or delete individual entries later. | | `totalFaqs` | integer | Total number of FAQ entries now stored for this agent in the specified environment. | ## Errors **400 Bad Request** - Validation error ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "FAQ limit reached. Each agent supports a maximum of 100 FAQ entries." } ``` **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Limits | Limit | Value | | ---------------------------------------- | ----- | | Maximum FAQs per agent (per environment) | 100 | | Maximum question phrasings per FAQ entry | 10 | An agent can hold a maximum of **100 FAQ entries** per environment. Attempting to add more will return HTTP 400. Use **List FAQs** to check the current count before adding new entries. Add multiple question phrasings per entry (up to 10) to increase the likelihood of a match. For example, "What are your hours?", "When do you open?", and "Are you open on weekends?" can all map to the same answer. Adding FAQ entries automatically enables FAQ matching on the agent (`faqEnabled = true`). To disable FAQ matching without deleting entries, update the agent with `faqEnabled: false` via **Update Agent**. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Create Agent Source: https://docs.gnani.ai/Platform/Create_Agent POST /v1/agents Create a new voice agent and get a botId **What it does:** Creates a new voice agent and returns a `botId`. The agent starts with dashboard defaults — you configure voice, prompt, and integrations in a follow-up [Update Agent](/Platform/Update_Agent) call. ```text theme={null} 1. POST /v1/agents → get botId (this page) 2. PUT /v1/agents/{botId} → set prompt, voice, webhook 3. POST /v1/agents/{botId}/trigger_call → test a call ``` ## Required Permission `agents` ## Minimum working example ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botName": "Support Agent", "description": "Handles inbound customer support calls.", "region": "asia", "timeZone": "Asia/Kolkata" }' ``` Save the `botId` from the response — you need it for every other Agents API call. ### Success (201) ```json theme={null} { "status": "success", "message": "Agent created successfully.", "response": { "botId": "fb79920229d144608ebf665a10e50275", "botName": "Support Agent" }, "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` ## Request body | Field | Required | Default | Description | | ------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------- | | `botName` | Yes | — | Unique name within your organization. | | `description` | No | `""` | Short description of the agent's purpose. | | `region` | No | `"asia"` | Deployment region — `asia`, `america`, or `europe`. See [Get Regions](/Platform/Get_Regions). | | `timeZone` | No | `"Asia/Kolkata"` | IANA timezone. See [Get Timezones](/Platform/Get_Timezones). | New agents inherit sensible defaults: barge-in enabled, TTS caching on, LLM temperature `0.5`, max tokens `300`. Override any setting via [Update Agent](/Platform/Update_Agent). ## What to do next | Step | Endpoint | Why | | -------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------- | | Configure voice and prompt | [Update Agent](/Platform/Update_Agent) | Set ASR, LLM, TTS, system prompt, webhook | | Validate prompt syntax | [Validate Prompt](/Platform/Validate_Prompt) | Check Jinja2 variables before saving | | Pick compatible ASR/TTS | [Get Transcriber Config](/Platform/Get_Transcriber_Config) | Ensure models support your languages | | Place a test call | [Trigger Call](/Platform/Trigger_Call) | Requires whitelisted phone number | | Review the call | [Get Conversation Logs](/Platform/Get_Conversation_Logs) → [Stats](/Platform/Get_Conversation_Stats) | Read disposition and transcript | ## Edge cases | Scenario | What happens | | --------------------------------- | ------------------------------------------ | | Duplicate `botName` | HTTP `409` — pick a unique name | | QA role without create permission | HTTP `403` | | Invalid `region` or `timeZone` | HTTP `400` — validate via Config API first | ## Errors **409 Conflict — name already taken** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "An agent named 'Support Agent' already exists in your organization" } ``` **403 Forbidden — insufficient permissions** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "QA role does not have permission to create/update agents" } ``` Call [Get Regions](/Platform/Get_Regions) and [Get Timezones](/Platform/Get_Timezones) before your first create request so you use valid values. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Delete Agent Source: https://docs.gnani.ai/Platform/Delete_Agent DELETE /v1/agents/{botId} Permanently delete an agent and all associated configuration **What it does:** Permanently delete a voice agent and all of its associated configuration from your organization. This action cannot be undone. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Minimum working example ```bash theme={null} curl -X DELETE "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent deleted successfully", "response": { "botId": "fb79920229d144608ebf665a10e50275" } } ``` ## Errors **404 Not Found** - Agent not found or not accessible ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found or does not belong to your organization" } ``` ## Use Cases * Removing outdated or unused agents from your organization * Cleaning up test agents after development * Decommissioning agents after a project is completed This action is permanent and cannot be undone. All agent configuration, FAQ entries, and settings will be removed. Ensure you have a backup of any configuration you may need before deleting. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Delete FAQs Source: https://docs.gnani.ai/Platform/Delete_FAQs DELETE /v1/agents/{botId}/faqs Delete one or more FAQ entries from an agent **What it does:** Permanently remove one or more FAQ entries from an agent by providing their `faqId` values. If all entries are deleted, `faqEnabled` is automatically set to `false` on the agent. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Body | Field | Type | Required | Description | | ------------- | --------- | -------- | -------------------------------------------------------------- | | `botId` | string | Yes | Must match the `botId` path parameter. | | `environment` | string | Yes | Target environment: `development`, `staging`, or `production`. | | `faqIds` | string\[] | Yes | List of FAQ entry IDs to delete. | ## Minimum working example ```bash theme={null} curl -X DELETE "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/faqs" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botId": "fb79920229d144608ebf665a10e50275", "environment": "development", "faqIds": ["faq_xyz001"] }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Successfully deleted 1 FAQ(s)", "response": { "deletedCount": 1, "deletedFaqIds": ["faq_xyz001"], "remainingFaqs": 0 } } ``` ## Response Fields | Field | Type | Description | | --------------- | --------- | ------------------------------------------- | | `deletedCount` | integer | Number of entries successfully deleted. | | `deletedFaqIds` | string\[] | IDs of the deleted entries. | | `remainingFaqs` | integer | Total FAQ entries remaining after deletion. | ## Errors **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Removing outdated or incorrect FAQ entries * Clearing the knowledge base before a full re-import * Freeing up capacity within the 100-entry limit This action is permanent. Confirm the IDs with **List FAQs** before deleting. Deleting all entries automatically sets `faqEnabled = false` on the agent. Adding new entries via **Add FAQs** or **Import FAQs** will re-enable it. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Error Reference Source: https://docs.gnani.ai/Platform/Error_Reference HTTP status codes and known error messages for the Agent Builder Platform API **What it does:** Reference all error types returned by the Agent Builder Platform API. All error responses follow the standard envelope format: ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Human-readable error description." } ``` When contacting Gnani support, always include the **`requestId`** from the error response. This UUID uniquely identifies the failed request in the platform logs and allows the engineering team to diagnose the issue quickly. *** ## HTTP Status Codes | Status Code | Name | When It Occurs | | ----------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **200** | OK | The request was successful. | | **201** | Created | A new resource (agent, FAQ) was created successfully. | | **400** | Bad Request | The request body or query parameters are invalid, missing required fields, or fail a business rule (e.g. FAQ limit reached, incompatible ASR model). | | **401** | Unauthorized | The API key is missing, invalid, or expired. | | **403** | Forbidden | The API key is valid but does not have the required permission for this endpoint, or the user role (e.g. QA) does not have write access. | | **404** | Not Found | The requested resource does not exist — unknown or inaccessible `botId`, conversation ID, FAQ entry, audio recording, or invalid config path (e.g. unknown region ID, transcriber language). | | **409** | Conflict | A resource with the same name already exists (e.g. duplicate agent name). | | **429** | Too Many Requests | The request rate limit has been exceeded. Wait before retrying. | | **500** | Internal Server Error | An unexpected error occurred on the Agent Builder platform. Retry the request. If the error persists, contact support with the `requestId`. | *** ## Known Error Messages The table below lists the specific error messages returned in the `message` field of the JSON response body, along with the HTTP status code and the condition that triggers each one. | Status | Error Message | Condition | | ------ | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `400` | `Maximum 3 languages are allowed` | `botDetails.language` contains more than 3 language codes. | | `400` | `At least one language is required` | `botDetails.language` is empty. | | `400` | `Unsupported language(s): [...]. Please use /platform/v1/agents/config/languages` | One or more language codes are not in the supported list. | | `400` | `Selected ASR provider does not support selected languages. Please check /platform/v1/agents/config/transcriber_config` | The chosen ASR provider is incompatible with one or more selected languages. | | `400` | `Selected ASR model does not support selected languages. Please check /platform/v1/agents/config/transcriber_config` | The chosen ASR model is incompatible with one or more selected languages. | | `400` | `Selected TTS provider does not support selected languages. Please check /platform/v1/agents/config/tts_config` | The chosen TTS provider is incompatible with one or more selected languages. | | `400` | `Selected TTS voice does not support selected languages. Please check /platform/v1/agents/config/tts_config` | The chosen TTS voice is incompatible with one or more selected languages. | | `400` | ``Field `phraseConfig.phrase` exceeds maximum of 100 entries.`` | The custom vocabulary list contains more than 100 phrases. | | `400` | ``Field `phraseConfig` is not supported for ASR model `{model}`.`` | Custom vocabulary was sent for an ASR model that does not support it. | | `400` | ``Field `speechSegmentationSilenceTimeout` is not supported for ASR model `{model}`.`` | The field was sent for an incompatible ASR model. | | `400` | ``Field `speechInitialSilenceTimeout` value {v} exceeds maximum of {max} seconds for ASR model `{model}`.`` | `speechInitialSilenceTimeout` exceeds the 30-second limit for streaming ASR models. | | `400` | ``Field `minWordsForBargeIn` is not supported for non-streaming ASR model `{model}`.`` | The field was sent for a REST (non-streaming) ASR model. | | `400` | ``Field `minTimeToBarge` is not supported for streaming ASR model `{model}`.`` | The field was sent for a streaming ASR model. | | `400` | ``Field `asrPreemptive` (Fast Streaming) is not supported for ASR model `{model}`.`` | The field was sent for an ASR model that does not support fast streaming. | | `400` | `languageSwitchMode must be 'implicit' or 'explicit'` | An invalid `languageSwitchMode` value was provided. | | `400` | `minWordsForLangSwitch must be >= 1` | The `minWordsForLangSwitch` value is less than 1. | | `403` | `QA role does not have permission to create/update agents` | The API key is associated with a user in the QA role. | | `404` | `Agent not found or does not belong to your organization` | The `botId` does not exist or is not accessible to your API key. Returned by Get, Update, and Delete Agent. | | `404` | `Agent not found` | The `botId` does not exist or is not accessible. Returned by FAQ and Chat SDK endpoints scoped to an agent. | | `404` | `Conversation not found` | The `conversationId` does not exist or is not accessible. | | `404` | `Audio file not found at path: ...` | No recording exists for this conversation (recording disabled, call too short, or file not yet available). | | `409` | `An agent named '...' already exists in your organization` | A duplicate agent name was used on create or rename. | *** ## Handling Errors in Code ### Example: Check Status and Handle Errors ```javascript theme={null} const response = await fetch( 'https://api.inya.ai/platform/v1/agents', { method: 'POST', headers: { 'x-api-key': '', 'Content-Type': 'application/json' }, body: JSON.stringify({ botName: 'Support Agent', region: 'asia', timeZone: 'Asia/Kolkata' }) } ); const body = await response.json(); if (body.status !== 'success') { console.error(`Request failed [${body.requestId}]: ${body.message}`); // Store body.requestId for support escalation } ``` ```python theme={null} import requests response = requests.post( 'https://api.inya.ai/platform/v1/agents', headers={'x-api-key': ''}, json={'botName': 'Support Agent', 'region': 'asia', 'timeZone': 'Asia/Kolkata'} ) body = response.json() if body.get('status') != 'success': print(f"Request failed [{body.get('requestId')}]: {body.get('message')}") # Store body['requestId'] for support escalation ``` *** ## Rate Limits The Platform API enforces rate limits to ensure fair usage. When exceeded, the API returns **HTTP 429**. Implement exponential backoff in your retry logic: ```python theme={null} import time def call_with_retry(func, max_retries=3): for attempt in range(max_retries): response = func() if response.status_code != 429: return response wait = 2 ** attempt # 1s, 2s, 4s print(f"Rate limited. Retrying in {wait}s...") time.sleep(wait) raise Exception("Max retries exceeded") ``` Store the `requestId` from every API response in your application logs. This makes it straightforward to correlate your own logs with Agent Builder platform logs when troubleshooting. # Get FAQ Sample Source: https://docs.gnani.ai/Platform/FAQ_Sample GET /v1/agents/faqs/sample Download a sample FAQ template showing the expected import format **What it does:** Download a sample FAQ template that shows the expected JSON structure for a bulk import request. Use this as a starting point when preparing your FAQ dataset before calling **Import FAQs**. ## Required Permission `agents` ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/faqs/sample" \ -H "x-api-key: " ``` ## Response ### Success (200) Returns a JSON response with a sample FAQ structure. The sample uses display names (`english`, `hindi`); when calling **Add FAQs** or **Import FAQs**, use BCP-47 codes from [Get Languages](/Platform/Get_Languages) (e.g. `en-IN`, `hi-IN`). ```json theme={null} { "status": "success", "message": "FAQ sample retrieved successfully", "response": { "faq": [ { "questions": [ "What are your business hours?", "When are you open?" ], "answer": "We are open Monday to Friday, 9am to 6pm IST.", "language": "english" } ] } } ``` ## Response Fields | Field | Type | Description | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- | | `faq[].questions` | string\[] | Sample question phrasings. Replace with your own questions. | | `faq[].answer` | string | Sample answer text. Replace with your actual answer. | | `faq[].language` | string | Language label in the sample (`english`, `hindi`). Replace with BCP-47 codes (e.g. `en-IN`, `hi-IN`) in Add/Import requests. | ## Errors **401 Unauthorized** - Invalid or missing API key ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Invalid API key" } ``` **500 Internal Server Error** - Template unavailable ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Internal server error" } ``` See [Error Reference](/Platform/Error_Reference) for other status codes. ## Use Cases * Understanding the expected JSON structure before building a large FAQ dataset * Sharing the template with content teams who will populate the entries * Validating that your export tooling produces the correct format before running a bulk import Populate the `faq` array with your entries and submit it to **Import FAQs** to load your FAQ knowledge base in bulk. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Agent Source: https://docs.gnani.ai/Platform/Get_Agent GET /v1/agents/{botId} Retrieve the complete configuration of a specific agent **What it does:** Retrieve the full configuration of a specific voice agent, including ASR, LLM, and TTS settings, advance ASR parameters, pre-call variables, voicemail detection, call transfer configuration, post-call webhook, and FAQ status. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " ``` ## Response Fields | Field | Type | Description | | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `botId` | string | Agent identifier. | | `botName` | string | Agent display name. | | `environment` | string | Always returns `development`. | | `botDetails` | object | `language[]`, `region`, `timeZone`, `description`. | | `overview` | object | `greetingMessage`, `endMessage`, `systemPrompt`, `hasDynamicGreeting`, `dynamicInitialMessageConfig`, `languageSwitchPromptDetails`, `languageSwitch`, `minWordsForLangSwitch`. | | `asrParams` | object | `provider`, `model`, `language`. | | `llmParams` | object | `provider`, `model`, `temperature` (0.0–1.0), `maxTokens` (1–4096). | | `ttsParams` | object | `provider`, `voice`, `language`, `ttsRate` (0.5–2.0), `enableCaching`. | | `advanceSettings` | object | Full ASR advance settings. See table below. | | `preCall` | object | `preCallVariableStatus`, `preCallParams` with `primaryVariable` and `variables[]`. | | `enableVoiceMailDetection` | boolean | Whether voicemail detection is enabled. | | `voiceMailDetectionConfig` | object | `enabled`, `maxDetectionRetries`, `maxVoicemailMessageWaitTime`, `voiceMailResponse`, `postDetectionActions`. | | `callTransferConfig` | object | `description`, `defaultTransferMessage`, `details[]`. | | `callTransferConfigStatus` | boolean | Whether call transfer is currently enabled. | | `hasPostCallTrigger` | boolean | When `true`, the platform POSTs call data to `postCallTriggerAPIConfig.url` after each call ends. See [Update Agent — Post-call webhook](/Platform/Update_Agent#post-call-webhook). | | `postCallTriggerAPIConfig` | object | Webhook URL, HTTP method, and optional headers. Only used when `hasPostCallTrigger` is `true`. | | `faqEnabled` | boolean | `true` once at least one FAQ has been added or imported. | ### `advanceSettings` Fields | Field | Type | Default | Range / Notes | | ---------------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------- | | `barge` | boolean | `true` | Allow the caller to interrupt the agent mid-speech. | | `initialMessageBarge` | boolean | `false` | Allow the caller to interrupt the opening greeting. | | `useDenoiser` | boolean | `true` | Apply background noise filtering to incoming audio. | | `suppressionLevel` | integer | `80` | Denoiser suppression intensity: 20–100. Active only when `useDenoiser` is `true`. | | `enableItnForAsr` | boolean | `false` | Apply Inverse Text Normalization to ASR output. | | `maxSpeechDuration` | number | `30` | Maximum single-utterance duration in seconds: 1–240. | | `speechInitialSilenceTimeout` | number | `10.0` | Silence threshold before ASR starts listening: 10–30s (streaming) or 10–60s (others). | | `endSilenceTimeout` | number | `800` | End-of-speech silence in milliseconds. Stored directly. | | `speechSegmentationSilenceTimeout` | number | `1000` | Silence gap used to segment utterances: 0.1–5s. Streaming models only. | | `minWordsForBargeIn` | integer | `1` | Minimum word count before a barge-in is accepted: 1–5. Streaming ASR only. | | `minTimeToBarge` | number | `0.1` | Minimum elapsed time (seconds) before barge-in is accepted: 0.1–3s. Non-streaming ASR only. | | `asrPreemptive` | boolean | `false` | Enable Fast Streaming mode. Specific REST ASR models only. | | `enableDtmf` | boolean | `false` | Accept DTMF (touch-tone) keypad input. | | `phraseConfig.phrase` | string\[] | `[]` | Custom vocabulary hint list. Maximum 100 phrases. Vocabulary-capable ASR models only. | | `phraseConfig.phraseWeight` | number | `0.0` | Bias weight applied to custom vocabulary: 0.0–2.0. | ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent details retrieved successfully", "response": { "botId": "fb79920229d144608ebf665a10e50275", "botName": "Support Agent", "environment": "development", "createdAt": "2026-04-01T10:00:00Z", "updatedAt": "2026-07-01T12:00:00Z", "botDetails": { "language": ["en-IN", "hi-IN"], "region": "asia", "timeZone": "Asia/Kolkata", "description": "Handles inbound customer support calls." }, "overview": { "greetingMessage": "Hello, how can I help you today?", "endMessage": "Thank you. Have a great day!", "systemPrompt": "You are a helpful support agent.", "hasDynamicGreeting": false, "dynamicInitialMessageConfig": { "hasHeaders": false, "method": null, "url": null, "headers": null }, "languageSwitch": true, "explicitLanguageSwitch": true, "minWordsForLangSwitch": 2, "languageSwitchPromptDetails": { "switchPrompt": "Which language do you prefer?", "mode": "explicit" } }, "asrParams": { "provider": "gnani", "model": "gnani_asr", "language": "en-IN" }, "llmParams": { "provider": "gnani", "model": "Pampa Go", "temperature": 0.5, "maxTokens": 300 }, "ttsParams": { "provider": "gnani", "voice": "Pranav", "language": "en-IN", "ttsRate": 1.0, "enableCaching": true }, "advanceSettings": { "barge": true, "initialMessageBarge": false, "useDenoiser": true, "suppressionLevel": 80, "enableItnForAsr": false, "maxSpeechDuration": 30.0, "speechInitialSilenceTimeout": 10.0, "endSilenceTimeout": 800.0, "speechSegmentationSilenceTimeout": 1000.0, "minWordsForBargeIn": 1, "minTimeToBarge": 0.1, "asrPreemptive": false, "enableDtmf": false, "phraseConfig": { "phrase": [], "phraseWeight": 0.0 } }, "preCall": { "preCallVariableStatus": true, "preCallParams": { "primaryVariable": "customer_name", "variables": [ { "id": "var_1", "variable": "customer_name", "sampleValue": "Alice", "description": "Customer name" } ] } }, "enableVoiceMailDetection": false, "voiceMailDetectionConfig": { "enabled": false, "maxDetectionRetries": 2, "maxVoicemailMessageWaitTime": 1, "voiceMailResponse": "We tried reaching you. Please call us back.", "postDetectionActions": [] }, "callTransferConfig": { "description": "Transfer to live agent", "defaultTransferMessage": "Please hold while we transfer you.", "details": [ { "phoneNumber": "9876500000", "countryCode": "+91", "prompt": "Transfer", "message": "Hold on" } ] }, "callTransferConfigStatus": false, "hasPostCallTrigger": true, "postCallTriggerAPIConfig": { "hasHeaders": false, "method": "POST", "url": "https://your-server.example.com/call_status", "headers": null }, "faqEnabled": false } } ``` ## Errors **404 Not Found** - Agent not found or not accessible ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found or does not belong to your organization" } ``` ## Use Cases * Reviewing an agent's full configuration before making updates * Auditing agent settings for compliance or quality assurance * Fetching the current configuration as a baseline before an **Update Agent** call * Checking `faqEnabled` to determine whether an agent has a FAQ knowledge base The response includes all configurable parameters for the agent. Use it to export or backup an agent's complete configuration before making significant changes. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Chat Widget Config Source: https://docs.gnani.ai/Platform/Get_Chat_SDK_Config GET /v1/agents/{botId}/chat-sdk-config Retrieve the current Chat Widget configuration for an agent **What it does:** Retrieve the complete Chat Widget (Chat SDK) configuration for a specific agent. This includes all visual customizations for the chat window and launcher button, the list of allowed origins for CORS, and whether the widget is currently enabled. Use this endpoint to inspect the current widget settings before making updates via **Update Chat Widget Config**. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Query Parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------- | | `environment` | string | No | Target environment. Default: `development`. | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/chat-sdk-config?environment=development" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "71b1f0ad-b3eb-46cc-b04d-37f020cbbe43", "message": "Chat SDK config retrieved successfully", "response": { "chatSdkConfig": { "enabled": true, "allowedOrigins": ["https://example.com"], "windowCustomizations": { "header": { "title": "Support Bot", "subtitle": "How can I help you?", "logo": "https://example.com/logo.svg", "backgroundColor": "#1A73E8", "textColor": "#FFFFFF" }, "body": { "backgroundColor": "#ffffff", "fontSize": "14px", "textColor": "#101828" }, "userMessage": { "backgroundColor": "#0087c7", "fontSize": "14px", "textColor": "#ffffff" }, "botMessage": { "backgroundColor": "#f9fafb", "fontSize": "14px", "textColor": "#101828" }, "footer": { "backgroundColor": "#ffffff", "textColor": "#101828", "fontSize": "14px", "infoText": "Powered by Gnani", "input": { "backgroundColor": "#ffffff", "borderColor": "#0087c7", "textColor": "#101828", "placeholder": "Type here..." }, "button": { "backgroundColor": "#101828", "textColor": "#ffffff" } } }, "widgetCustomizations": { "buttonStyles": { "backgroundColor": "#1A73E8", "width": "60px", "height": "60px" } }, "quickActions": null, "callTransfer": null, "clearChat": null } } } ``` ## Response Fields ### `response.chatSdkConfig` | Field | Type | Description | | ---------------- | -------------- | ---------------------------------------------------------------------------------- | | `enabled` | boolean | Whether the Chat Widget is currently active. | | `allowedOrigins` | string\[] | Whitelisted domains permitted to embed and use the widget (CORS). | | `quickActions` | object \| null | Quick-reply action buttons shown to the user. `null` if not configured. | | `callTransfer` | object \| null | Configuration for transferring the chat to a live agent. `null` if not configured. | | `clearChat` | object \| null | Configuration for the clear-chat control. `null` if not configured. | ### `windowCustomizations` | Section | Fields | Description | | ------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | `header` | `title`, `subtitle`, `logo`, `backgroundColor`, `textColor` | Chat window header bar appearance. | | `body` | `backgroundColor`, `fontSize`, `textColor` | Chat transcript area background and text styles. | | `userMessage` | `backgroundColor`, `fontSize`, `textColor` | Style for user-sent message bubbles. | | `botMessage` | `backgroundColor`, `fontSize`, `textColor` | Style for agent-sent message bubbles. | | `footer` | `backgroundColor`, `textColor`, `fontSize`, `infoText`, `input`, `button` | Footer bar including the text input field and send button. | ### `widgetCustomizations` | Section | Fields | Description | | -------------- | ------------------------------------ | ------------------------------------------------------------------- | | `buttonStyles` | `backgroundColor`, `width`, `height` | Styles for the floating launcher button that opens the chat window. | ## Errors **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Retrieving widget configuration before updating it * Displaying current widget settings in an administration dashboard * Exporting widget configuration for backup or audit purposes Fetch the current configuration before making updates so you can perform partial modifications without overwriting fields you intend to keep. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Conversation Logs Source: https://docs.gnani.ai/Platform/Get_Conversation_Logs POST /v1/conversations/logs List past calls and find conversationId values for Stats and Audio **What it does:** Returns a paginated list of past calls. Use it to find `conversationId` values, then call [Get Conversation Statistics](/Platform/Get_Conversation_Stats) for outcomes and full transcripts. ```text theme={null} Need call IDs? → POST /v1/conversations/logs (this page) Need outcome + transcript? → GET /v1/conversations/{id}/stats Need the recording? → GET /v1/conversations/{id}/audio ``` **Why POST and not GET?** This endpoint searches conversations using filters — date range, call status, search term, pagination — that belong in the **request body**, not the URL. A GET request cannot carry a JSON filter payload, so we use `POST /v1/conversations/logs` with filters in the body and only `botId` + `environment` on the query string. Think of it as *search conversations with filters*, not a simple list fetch. ## Required Permission `conversations` ## Minimum working example Copy this first. Add `?environment=development` unless your API key has production access. ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/conversations/logs?environment=development" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "pageNo": 1, "pageSize": 10 }' ``` Pick `conversationId` from `response.data[]`, then call Stats: ```bash theme={null} curl "https://api.inya.ai/platform/v1/conversations//stats" \ -H "x-api-key: " ``` ## Logs vs Stats — which one do I need? | Question | Use Logs | Use Stats | | ------------------------------------------ | ---------------------- | ----------------------------------------------------- | | List all calls from last week | Yes | No | | Get `conversationId` for one call | Yes | No | | Get `overallCallDisposition` (PTP, RNR, …) | No | Yes | | Get full turn-by-turn transcript | Partial / may be empty | Yes — `utteranceAnalytics` | | Download MP3 recording | No | No — use [Audio](/Platform/Stream_Conversation_Audio) | ## Parameters ### Query string | Name | Required | Description | | ------------- | --------- | -------------------------------------------------------------------------------------------- | | `environment` | Often yes | `development`, `staging`, or `production`. Omitting this may return `403` for some API keys. | | `botId` | No | Limit results to one agent. | ### Request body | Field | Required | Description | | ------------------- | -------- | ------------------------------------------------------------------------------ | | `pageNo` | Yes | Page number. Start at `1`. Sending `0` returns empty `data` (not an error). | | `pageSize` | Yes | Results per page. Max recommended: `100`. | | `searchTerm` | No | Search by caller name, keyword, or `conversationId`. | | `filter.startDate` | No | ISO 8601 start — e.g. `"2026-08-01T00:00:00.000Z"`. Use with `filter.endDate`. | | `filter.endDate` | No | ISO 8601 end. | | `filter.callStatus` | No | Telephony filter — e.g. `"ANSWERED"`, `"NO ANSWER"`. | | `filter.botId` | No | Same as query `botId`; can be set in either place. | ## Additional examples ### Search by conversation ID ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/conversations/logs?environment=development" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "searchTerm": "1786433682702486082", "pageNo": 1, "pageSize": 10 }' ``` ### Filter by agent and date range ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/conversations/logs?botId=fbec81f5c97c4a62b0103e87f53c0d56&environment=development" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "filter": { "startDate": "2026-08-01T00:00:00.000Z", "endDate": "2026-08-11T23:59:59.999Z" }, "pageNo": 1, "pageSize": 10 }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "message": "Conversations retrieved successfully", "response": { "pageNo": 1, "pageSize": 10, "totalCount": 26, "totalPages": 3, "data": [ { "conversationId": "1786433682702486082", "botId": "fbec81f5c97c4a62b0103e87f53c0d56", "botName": "Muthoot-sanity_outbound_", "startTime": 1786433683.0, "endTime": 1786433770.0, "name": "shyam.r@gnani.ai", "transcript": [] } ] }, "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` ### What each field means | Field | What it is | | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | `conversationId` | **Copy this.** Use it in Stats and Audio endpoints. | | `botId` / `botName` | Which agent handled the call. | | `startTime` / `endTime` | Unix timestamp (seconds) for call start and end. | | `name` | Caller label or session name from trigger data — **not** transcript text. | | `transcript` | Preview of what was said. Often **empty or partial** in the list view. Always use Stats for the full record. | ### Empty results (200) ```json theme={null} { "status": "success", "message": "Conversations retrieved successfully", "response": { "pageNo": 1, "pageSize": 10, "totalCount": 0, "totalPages": 0, "data": [] } } ``` ## Edge cases | Scenario | What happens | | --------------------- | ------------------------------------------------------------------------------------------------------ | | `pageNo: 0` | HTTP 200, empty `data` — not an error | | Missing `environment` | May return `403` | | No matching calls | `totalCount: 0`, `data: []` | | `transcript` is empty | Normal — call [Get Conversation Statistics](/Platform/Get_Conversation_Stats) for `utteranceAnalytics` | | Large date ranges | Set `pageSize: 100` and loop `pageNo` until it exceeds `totalPages` | ## Errors **403 Forbidden** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "User does not have access to conversation logs" } ``` Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Conversation Statistics Source: https://docs.gnani.ai/Platform/Get_Conversation_Stats GET /v1/conversations/{conversationId}/stats Get call outcome, disposition code, and full transcript for one conversation **What it does:** Returns everything about **one call** — whether the phone connected, the business outcome code, and the full transcript. **You need:** A `conversationId` from [Get Conversation Logs](/Platform/Get_Conversation_Logs) or a post-call webhook (`conversation_id`). After [Trigger Call](/Platform/Trigger_Call), poll Logs with your `clientReferenceId` to find the `conversationId`. ## Required Permission `conversations` ## Path Parameters | Name | Required | Description | | ---------------- | -------- | ------------------------------------------------------------ | | `conversationId` | Yes | From Logs or webhook. Not returned directly by Trigger Call. | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/conversations/1786433682702486082/stats" \ -H "x-api-key: " ``` ## The three fields developers ask about Calls produce three different kinds of outcome data. They answer different questions: | Field | Plain English | Example | Use it for | | ----------------------------- | ------------------------------------------------------- | ----------------------------------- | ---------------------------------------------- | | **`callStatus`** | Did the phone connect? | `"ANSWERED"`, `"NO ANSWER"` | Telephony dashboards — was the call picked up? | | **`overallCallDisposition`** | What was the **business result**? Short code. | `"PTP"`, `"RNR"`, `"DSCN"` | CRM updates, campaign reports, filtering | | **`callSummary.disposition`** | LLM-written label in plain language. **May be `null`.** | `"resolved"`, `"escalated"` | Human-readable summaries for QA review | | **`utteranceAnalytics`** | What was **actually said**, turn by turn. | `[{ role, content, timestamp, … }]` | Transcripts, compliance, debugging | ```text theme={null} Call ends │ ├─ callStatus → "Did they pick up?" (telephony) ├─ overallCallDisposition → "What happened commercially?" (short code — PTP, RNR…) ├─ callSummary.disposition → "How would an LLM describe it?" (optional, may be null) └─ utteranceAnalytics → "What was said word for word?" (full transcript) ``` ### `overallCallDisposition` — what is it? A **short outcome code** assigned after the call, based on your agent's disposition configuration. It is **not** a sentence — it is a token you map in your CRM and reporting pipeline. ## Disposition codes Standard `overallCallDisposition` values: | Code | Meaning | | ------ | ------------------ | | `PTP` | Promise to Pay | | `RTP` | Refused to Pay | | `AP` | Already Paid | | `CLBK` | Callback Requested | | `WRNG` | Wrong Number | | `DSCN` | Call Disconnected | | `RNR` | No Answer | | `DND` | Do Not Disturb | Your agent's disposition prompt may produce additional codes beyond this list. Treat unknown values as opaque strings and log them for mapping. The same code appears as `STAGE_CODE` and inside `disposition_result` in post-call webhooks (snake\_case) — see [Update Agent — Post-call webhook](/Platform/Update_Agent#post-call-webhook). ### `callSummary` — what is it? An optional LLM-generated wrapper. When present, `callSummary.disposition` gives a human-readable label (e.g. `"resolved"`). When analytics have not finished processing, **`callSummary` is `null`** — this is normal on recent calls. Use `overallCallDisposition` for programmatic logic. Use `callSummary.disposition` only when you need a readable label and the object is present. ### `utteranceAnalytics` — what is it? The full conversation transcript as an array of turns. Each entry has: | Sub-field | On | Description | | ---------------------- | ---------- | ----------------------------------------------- | | `role` | All turns | `"assistant"` or `"user"` | | `content` | All turns | What was spoken | | `timestamp` | All turns | Unix timestamp (seconds, may include fractions) | | `detectedLanguage` | User turns | Language detected — e.g. `"english"` | | `totalResults` | User turns | Transcript in each configured language | | `userInterruptionFlag` | User turns | Whether the user interrupted the agent | Same shape as `transcript` in Logs, but Stats always returns the complete record. Post-call webhooks deliver the same data in **snake\_case** (`conversation_id`, `detected_language`, `STAGE_CODE`). See [Update Agent — Post-call webhook](/Platform/Update_Agent#post-call-webhook) for the webhook payload and field mapping. ## Response ### Success (200) Verified example: ```json theme={null} { "status": "success", "message": "Conversation details fetched successfully", "response": [ { "userId": "29be0f97-51c6-49aa-a515-e0433649e69d", "bot": "fbec81f5c97c4a62b0103e87f53c0d56", "botName": "Muthoot-sanity_outbound_", "conversationId": "1786433682702486082", "startTime": 1786433683, "endTime": 1786433770, "callStatus": "ANSWERED", "callDuration": 81, "overallCallDisposition": "PTP", "utteranceAnalytics": [ { "role": "assistant", "content": "Hi,shyam1234 how are you doing just want to known did you pay the EMI due amount?", "timestamp": 1786433683.312441 }, { "role": "user", "content": "how much is my due amount", "timestamp": 1786433699.931499, "totalResults": { "en-IN": "how much is my due amount", "hi-IN": "हाउ मच इज़ माइ ड्यू अमाउंट" }, "detectedLanguage": "english", "userInterruptionFlag": false }, { "role": "assistant", "content": "Hello, am I speaking with Nani?", "timestamp": 1786433700.549843 }, { "role": "user", "content": "yes you are", "timestamp": 1786433705.403072, "totalResults": { "en-IN": "yes you are", "hi-IN": "येस शुर" }, "detectedLanguage": "english", "userInterruptionFlag": false }, { "role": "assistant", "content": "I'm Viraj calling from Muthoot Capital. I'm just calling to check in with you. Your loan payment of [due_amount] was due on [due_date], and we noticed it hasn't come through yet. I wanted to understand if there was any delay or difficulty from your side?", "timestamp": 1786433706.562911 }, { "role": "user", "content": "actually i will be paying it today", "timestamp": 1786433722.501854, "totalResults": { "en-IN": "actually i will be paying it today", "hi-IN": "अक्चवली आई विल बी पेइंग इट टुडे" }, "detectedLanguage": "english", "userInterruptionFlag": false }, { "role": "assistant", "content": "That is great to hear. Just to check, was there any specific reason for the delay this time?", "timestamp": 1786433723.086668 }, { "role": "user", "content": "my salary got delayed", "timestamp": 1786433734.365692, "totalResults": { "en-IN": "my salary got delayed", "hi-IN": "माय सैलरी गॉट डिलेड" }, "detectedLanguage": "english", "userInterruptionFlag": false }, { "role": "assistant", "content": "I understand. Salary delays can happen sometimes. Thank you for letting me know. Just to keep you informed, When a payment is missed, It may have a small impact on your credit score. Also, As per the loan terms, Late payment charges may be applied. That is why we reach out early, So you can avoid any extra charges.", "timestamp": 1786433735.428127 }, { "role": "user", "content": "okay thank you for that bye", "timestamp": 1786433757.258117, "totalResults": { "en-IN": "okay thank you for that bye", "hi-IN": "ओके थैंक यू फॉर दैट बाई" }, "detectedLanguage": "english", "userInterruptionFlag": false }, { "role": "assistant", "content": "Since you mentioned you will be paying today, I will just make a small note of it. Thank you for your time, Nani. If you need any assistance, Please feel free to reach out. Have a good day. Thank you, Have a nice day.", "timestamp": 1786433761.166623 } ], "botId": "fbec81f5c97c4a62b0103e87f53c0d56" } ], "requestId": "ed1c4163-8841-4316-9b27-68f20296537c" } ``` `callSummary`, `averageAgentLatency`, `callProcessed`, and `lastProcessedOn` are not shown above because they are populated asynchronously and may be absent on recent calls. When present: ```json theme={null} "callSummary": { "disposition": "resolved" }, "averageAgentLatency": 1.2, "callProcessed": true, "lastProcessedOn": "2026-08-11T10:30:00Z" ``` ## All response fields | Field | Type | Description | | ------------------------- | --------------- | -------------------------------------------------------------------- | | `conversationId` | string | Unique call identifier. | | `botId` | string | Agent that handled this call. | | `botName` | string | Agent display name. | | `callDuration` | number | Length in seconds. `0` when the call was not answered. | | `startTime` / `endTime` | number | Unix timestamps (seconds). | | `callStatus` | string | Telephony outcome — `ANSWERED`, `NO ANSWER`, etc. | | `overallCallDisposition` | string | Business outcome code — see [Disposition codes](#disposition-codes). | | `callSummary` | object \| null | LLM summary. `null` until analytics finish. | | `callSummary.disposition` | string | Human-readable LLM label when present. | | `utteranceAnalytics` | array | Full turn-by-turn transcript. | | `averageAgentLatency` | number \| null | Mean agent response time in seconds. May be absent. | | `callProcessed` | boolean \| null | `true` when analytics pipeline finished. May be absent. | | `lastProcessedOn` | string \| null | ISO 8601 timestamp of last analytics run. | ## Edge cases | Scenario | What happens | | ------------------------------ | ---------------------------------------------------------------------------------------------- | | Call just ended | `overallCallDisposition` and `utteranceAnalytics` available; `callSummary` may still be `null` | | Call not answered | `callStatus: "NO ANSWER"`, `callDuration: 0`, disposition often `RNR` | | Analytics still running | Retry after 30–60 seconds, or use post-call webhook for immediate push | | `averageAgentLatency` above 2s | Often indicates LLM or TTS settings worth tuning | ## Errors **403 Forbidden** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "User does not have access to this conversation" } ``` **404 Not Found** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Conversation not found" } ``` Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get LLM Models Source: https://docs.gnani.ai/Platform/Get_LLM_Models GET /v1/agents/config/llm_models Retrieve available Large Language Model providers and models for agent configuration **What it does:** Retrieve all available LLM (Large Language Model) providers and their supported models. The LLM is the reasoning engine of your agent — it processes user input, maintains conversation context, and generates responses based on the system prompt. Use this endpoint to populate model selection options when configuring an agent's `llmParams`. ## Required Permission `agents` ## Query Parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `provider` | string | No | Filter results to a specific LLM provider (e.g. `gnani`, `openai`). When omitted, all available providers and models are returned. | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/llm_models" \ -H "x-api-key: " ``` ## Additional examples ### Filter by Provider ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/llm_models?provider=gnani" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Models retrieved successfully", "response": { "LLM": [ { "name": "Anthropic", "provider": { "providerId": "anthropic", "label": "Anthropic", "functionCalling": true, "multilingual": true }, "models": [ { "modelId": "claude-3-5-sonnet-20241022", "label": "Claude-3-5-Sonnet", "functionCalling": true, "supportedLanguages": ["en-IN", "hi-IN", "ta-IN", "kn-IN", "en-US"] } ] }, { "name": "Gnani", "provider": { "providerId": "gnani", "label": "Gnani", "functionCalling": true, "multilingual": true }, "models": [ { "modelId": "Pampa Go", "label": "Pampa Go", "functionCalling": true, "supportedLanguages": ["en-IN", "hi-IN", "ta-IN", "kn-IN", "en-US"] }, { "modelId": "Pampa Priority", "label": "Pampa Priority", "functionCalling": true, "supportedLanguages": ["en-IN", "hi-IN", "ta-IN", "kn-IN", "en-US"] } ] }, { "name": "OpenAI", "provider": { "providerId": "openai", "label": "Open AI", "functionCalling": true, "multilingual": true }, "models": [ { "modelId": "gpt-4.1-mini", "label": "GPT-4.1-MINI", "functionCalling": true, "supportedLanguages": ["en-IN", "hi-IN", "en-US"] }, { "modelId": "gpt-4o-mini", "label": "GPT-4o Mini", "functionCalling": true, "supportedLanguages": ["en-IN", "hi-IN", "en-US"] } ] } ] } } ``` ## Response Fields | Field | Type | Description | | ----------------------------------- | --------- | -------------------------------------------------------------------------------------------- | | `LLM[].name` | string | Human-readable provider display name. | | `LLM[].provider.providerId` | string | Provider identifier. Use this as the `provider` value in `llmParams` when updating an agent. | | `LLM[].provider.label` | string | Human-readable provider label. | | `LLM[].provider.functionCalling` | boolean | Whether this provider supports function calling. | | `LLM[].provider.multilingual` | boolean | Whether this provider supports multilingual agents. | | `LLM[].models[].modelId` | string | Unique model identifier. Use this as the `model` value in `llmParams`. | | `LLM[].models[].label` | string | Human-readable model display name. | | `LLM[].models[].functionCalling` | boolean | Whether this specific model supports function calling. | | `LLM[].models[].supportedLanguages` | string\[] | BCP-47 language codes supported by this model. | ## Errors **401 Unauthorized** - Invalid or missing API key ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Invalid API key" } ``` An unknown `provider` filter value returns HTTP `200` with an empty `LLM` array — not an error. See [Error Reference](/Platform/Error_Reference) for other status codes. ## Use Cases * Populating an LLM provider and model selector during agent creation or configuration * Allowing users to switch between language models (e.g. from a smaller model to a larger one) * Filtering models by provider when your organization has a specific LLM contract The `provider.providerId` and `modelId` values returned here map directly to the `provider` and `model` fields in the **Update Agent** `llmParams` object. Use them as-is. Use the `provider` filter to show only the models available from your preferred LLM vendor, simplifying the selection UI for your users. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Languages Source: https://docs.gnani.ai/Platform/Get_Languages GET /v1/agents/config/languages Retrieve the list of supported languages for agent configuration **What it does:** Retrieve the full list of languages supported by the Agent Builder platform. The returned language codes follow the BCP-47 standard (e.g. `en-IN`, `hi-IN`) and are used across all other configuration endpoints — including **Get Transcriber Config** and **Get TTS Config** — to discover compatible providers for your target languages. ## Required Permission `agents` ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/languages" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Language data retrieved successfully", "response": { "languages": [ { "langId": "en-IN", "name": "English (IN)", "pair": [ { "langId": "hi-IN" }, { "langId": "kn-IN" }, { "langId": "ta-IN" }, { "langId": "te-IN" } ] }, { "langId": "hi-IN", "name": "Hindi", "pair": [ { "langId": "en-IN" }, { "langId": "kn-IN" }, { "langId": "ta-IN" } ] }, { "langId": "ta-IN", "name": "Tamil", "pair": [ { "langId": "hi-IN" }, { "langId": "en-IN" } ] }, { "langId": "te-IN", "name": "Telugu", "pair": [ { "langId": "hi-IN" }, { "langId": "en-IN" } ] }, { "langId": "kn-IN", "name": "Kannada", "pair": [ { "langId": "hi-IN" }, { "langId": "en-IN" } ] } ] } } ``` ## Response Fields | Field | Type | Description | | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `langId` | string | BCP-47 language code. Use this value in `botDetails.language[]` and as the `language` query parameter for transcriber and TTS config lookups. | | `name` | string | Human-readable display name of the language. | | `pair` | object\[] | List of language codes that can be paired with this language for multilingual agents. Each object contains a `langId` field. | ## Errors **404 Not Found** - Language configuration unavailable ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Language configuration not found" } ``` ## Use Cases * Populating a language dropdown when creating or updating an agent * Passing language codes to **Get Transcriber Config** and **Get TTS Config** to filter compatible providers * Displaying supported languages in a settings UI or documentation Language codes returned here (e.g. `en-IN`) are the exact values expected by the `language` query parameter in **Get Transcriber Config** and **Get TTS Config**. Use them directly to filter compatible providers and voices. The supported language list grows over time. Always fetch this endpoint dynamically rather than hardcoding language codes in your application. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Regions Source: https://docs.gnani.ai/Platform/Get_Regions GET /v1/agents/config/regions Retrieve the list of available deployment regions for agent hosting **What it does:** Retrieve all available deployment regions where agents can be hosted. Each region represents a geographic cluster with dedicated infrastructure optimized for low-latency voice interactions. The region you choose when creating an agent determines where audio processing and AI inference run. ## Required Permission `agents` ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/regions" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Region data retrieved successfully", "response": { "regions": [ { "regId": "asia", "name": "Asia" }, { "regId": "america", "name": "Americas" }, { "regId": "europe", "name": "Europe" } ] } } ``` ## Response Fields | Field | Type | Description | | ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `regId` | string | Unique region identifier. Use this value as the `region` field in **Create Agent** and as the `regId` path parameter in **Get Timezones**. | | `name` | string | Human-readable region display name. | ## Errors **401 Unauthorized** - Invalid or missing API key ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Invalid API key" } ``` See [Error Reference](/Platform/Error_Reference) for other status codes. ## Use Cases * Populating a region selector in a new agent creation flow * Retrieving the `regId` needed to call **Get Timezones** for timezone selection * Presenting region options in a configuration dashboard The `regId` returned here is used directly as the `region` field in **Create Agent** and as the `{regId}` path parameter in **Get Timezones**. For example: `asia`, `america`, or `europe`. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get TTS Config Source: https://docs.gnani.ai/Platform/Get_TTS_Config GET /v1/agents/config/tts_config Retrieve available Text-to-Speech providers and voices for agent voice output settings **What it does:** Retrieve available TTS (Text-to-Speech) providers, models, and voices filtered by language. The TTS engine is the agent's "voice" — it converts the LLM's text responses into spoken audio that the caller hears. Each voice entry includes metadata such as gender and a sample audio URL so you can preview voices before selecting one. The `language` parameter is **required**. For multilingual agents, pass all language codes together to return only voices that support every language in the set. ## Required Permission `agents` ## Query Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `language` | string | **Yes** | Filter by one or more language codes. Repeat this parameter for multiple languages (e.g. `?language=en-IN&language=hi-IN`). Returns only voices that support **all** specified languages. | | `providerId` | string | No | Filter results to a specific TTS provider (e.g. `gnani`). | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/tts_config?language=en-IN" \ -H "x-api-key: " ``` ## Additional examples ### Multiple languages ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/tts_config?language=en-IN&language=hi-IN" \ -H "x-api-key: " ``` ### Filter by provider ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/tts_config?language=en-IN&providerId=gnani" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "TTS config retrieved successfully", "response": { "ttsConfig": [ { "name": "Gnani", "provider": { "providerId": "vachana_timbre-v2.5", "label": "Gnani" }, "models": [ { "modelId": "vachana-voice-v1-standard-streaming", "label": "Gnani Vachana", "supportedLanguages": ["en-IN", "hi-IN", "kn-IN", "ta-IN", "te-IN"], "voices": [ { "voiceId": "Pranav", "name": "Pranav", "label": "Pranav", "supportedLanguages": ["en-IN", "hi-IN"], "isMultilingual": true, "gender": "male", "demoAudioUrl": "", "accent": [], "tone": [] }, { "voiceId": "Shubhra", "name": "Shubhra", "label": "Shubhra", "supportedLanguages": ["en-IN", "hi-IN"], "isMultilingual": true, "gender": "female", "demoAudioUrl": "", "accent": [], "tone": [] } ] } ] } ] } } ``` ## Errors **404 Not Found** - Missing language parameter or no matching configuration ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Language parameter is empty", "response": { "ttsConfig": [] } } ``` ## Response Fields | Field | Type | Description | | -------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------- | | `ttsConfig[].name` | string | Human-readable provider display name. | | `ttsConfig[].provider.providerId` | string | TTS provider identifier. Use as the `provider` value in `ttsParams` when updating an agent. | | `ttsConfig[].provider.label` | string | Human-readable provider label. | | `ttsConfig[].models[].modelId` | string | Model identifier. | | `ttsConfig[].models[].label` | string | Human-readable model display name. | | `ttsConfig[].models[].supportedLanguages` | string\[] | BCP-47 language codes supported by this model. | | `ttsConfig[].models[].voices[].voiceId` | string | Voice identifier. Use as the `voice` value in `ttsParams`. | | `ttsConfig[].models[].voices[].name` | string | Human-readable voice display name. | | `ttsConfig[].models[].voices[].supportedLanguages` | string\[] | BCP-47 language codes this specific voice supports. | | `ttsConfig[].models[].voices[].isMultilingual` | boolean | Whether this voice can speak across multiple languages. | | `ttsConfig[].models[].voices[].gender` | string | Voice gender: `male` or `female`. | | `ttsConfig[].models[].voices[].demoAudioUrl` | string | URL to a preview audio clip. Use this to let users listen to the voice before selecting it. | | `ttsConfig[].models[].voices[].accent` | string\[] | Accent tags associated with this voice. | | `ttsConfig[].models[].voices[].tone` | string\[] | Tone tags associated with this voice. | ## Use Cases * Populating a voice selector with audio preview in agent configuration * Filtering TTS voices to only those compatible with the agent's configured languages * Comparing voice options across providers to best match your brand's tone * Building a voice gallery or preview feature using `sample_audio_url` The `language` parameter is **required**. A request without at least one language value will return a 404 error. Use language codes from **Get Languages**. For multilingual agents, pass all configured language codes together (e.g. `?language=en-IN&language=hi-IN`) to find voices that support every language. This prevents selecting a voice that only works in one of the configured languages. The `provider.providerId` and `voiceId` values map directly to the `provider` and `voice` fields in the **Update Agent** `ttsParams` object. Use the `demoAudioUrl` to build an audio preview feature so users can hear voices before committing to a selection. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Timezones Source: https://docs.gnani.ai/Platform/Get_Timezones GET /v1/agents/config/timezone/{regId} Retrieve the list of available timezones for a specific deployment region **What it does:** Retrieve all IANA timezone identifiers available within a specific deployment region. Use the `regId` returned by **Get Regions** to load the timezone options for that region. The selected timezone controls how the Agent Builder platform interprets scheduled events, analytics timestamps, and call scheduling. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------------------------------------------------------- | | `regId` | string | Yes | Region identifier from **Get Regions** (e.g. `asia`, `america`, `europe`). | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/timezone/asia" \ -H "x-api-key: " ``` ## Additional examples ### Americas region ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/timezone/america" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Timezones retrieved successfully", "response": { "timezones": [ { "timeId": "Asia/Kolkata", "name": "Indian Standard Time (IST)" }, { "timeId": "Asia/Singapore", "name": "Singapore Standard Time (SST)" }, { "timeId": "Asia/Tokyo", "name": "Japan Standard Time (JST)" }, { "timeId": "Asia/Dubai", "name": "Gulf Standard Time (GST)" }, { "timeId": "Asia/Bangkok", "name": "Indochina Time (ICT)" } ] } } ``` ## Errors **404 Not Found** - Invalid or unrecognized region identifier ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Region not found" } ``` ## Response Fields | Field | Type | Description | | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- | | `timezones[].timeId` | string | IANA timezone identifier (e.g. `Asia/Kolkata`). Use this as the `timeZone` field in **Create Agent** or **Update Agent**. | | `timezones[].name` | string | Human-readable timezone name with abbreviation (e.g. `Indian Standard Time (IST)`). | ## Use Cases * Building a cascading region → timezone selector in an agent creation UI * Validating timezone values before sending a **Create Agent** or **Update Agent** request * Displaying timezone context in an agent settings dashboard Build a cascading selection flow: call **Get Regions** first to let the user pick a region, then call this endpoint to populate the timezone dropdown with the relevant options for that region. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Transcriber Config Source: https://docs.gnani.ai/Platform/Get_Transcriber_Config GET /v1/agents/config/transcriber_config Retrieve available ASR providers and models for agent transcription settings **What it does:** Retrieve available ASR (Automatic Speech Recognition) providers and their models, filtered by language. The transcriber is the agent's "ears" — it converts the caller's spoken audio into text for the LLM to process. Use this endpoint to identify which ASR models support the languages your agent is configured to use. The `language` parameter is **required**. For multilingual agents (up to 3 languages), pass all language codes together so only models that support every language in the combination are returned. ## Required Permission `agents` ## Query Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `language` | string | **Yes** | Filter by one or more language codes. Repeat this parameter for multiple languages (e.g. `?language=en-IN&language=hi-IN`). Returns only ASR models that support **all** specified languages. | | `providerId` | string | No | Filter results to a specific ASR provider (e.g. `gnani`, `deepgram`). | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/transcriber_config?language=en-IN" \ -H "x-api-key: " ``` ## Additional examples ### Multilingual — filter by multiple languages ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/transcriber_config?language=en-IN&language=hi-IN" \ -H "x-api-key: " ``` ### Filter by language and provider ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/config/transcriber_config?language=en-IN&providerId=gnani" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Transcriber config retrieved successfully", "response": { "transcriberConfig": [ { "name": "Gnani", "provider": { "providerId": "gnani", "label": "Gnani", "description": "Purpose-built for Indian languages and accents. Best for Indic and multilingual Indian agents." }, "models": [ { "modelId": "gnani_vachana_rest", "label": "Vachana Rest", "supportedLanguages": ["hi-IN", "en-IN", "te-IN", "ta-IN", "mr-IN", "bn-IN", "kn-IN", "ml-IN"], "isMultilingual": true, "modelTier": "balanced" }, { "modelId": "gnani_v2", "label": "Vachana", "supportedLanguages": ["hi-IN", "en-IN", "te-IN", "ta-IN", "mr-IN", "bn-IN", "kn-IN", "ml-IN"], "isMultilingual": true, "modelTier": "custom" } ] }, { "name": "Microsoft", "provider": { "providerId": "microsoft", "label": "Microsoft", "description": "Strong accuracy for English and international languages. Recommended for non-Indic agents." }, "models": [ { "modelId": "microsoft_rest", "label": "Rest", "supportedLanguages": ["en-IN", "hi-IN", "en-US", "ta-IN", "te-IN"], "isMultilingual": true, "modelTier": "balanced" }, { "modelId": "microsoft_streaming", "label": "Streaming", "supportedLanguages": ["en-IN", "hi-IN", "en-US", "ta-IN", "te-IN"], "isMultilingual": true, "modelTier": "responsive" } ] } ] } } ``` ## Errors **404 Not Found** - No matching ASR configuration found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Transcriber config not found", "response": { "transcriberConfig": [] } } ``` ## Response Fields | Field | Type | Description | | ------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------- | | `transcriberConfig[].name` | string | Human-readable provider display name. | | `transcriberConfig[].provider.providerId` | string | ASR provider identifier. Use as the `provider` value in `asrParams` when updating an agent. | | `transcriberConfig[].provider.label` | string | Human-readable provider label. | | `transcriberConfig[].provider.description` | string | Brief description of the provider's strengths and recommended use cases. | | `transcriberConfig[].models[].modelId` | string | Model identifier. Use as the `model` value in `asrParams`. | | `transcriberConfig[].models[].label` | string | Human-readable model display name. | | `transcriberConfig[].models[].supportedLanguages` | string\[] | BCP-47 language codes supported by this model. | | `transcriberConfig[].models[].isMultilingual` | boolean | Whether this model can handle multiple languages in a single session. | | `transcriberConfig[].models[].modelTier` | string | Performance tier of the model: `balanced`, `responsive`, or `custom`. | ## Use Cases * Identifying which ASR models support a specific language or language combination before configuring an agent * Filtering the transcription model list to only show compatible options in a configuration UI * Validating that your current ASR model still supports a new language you are adding to a multilingual agent * Cross-referencing with the **Update Agent** `advanceSettings` compatibility table to verify model-specific features The `provider.providerId` and `modelId` values returned here map directly to the `provider` and `model` fields in the **Update Agent** `asrParams` object. Use them as-is. The `language` parameter is **required**. Requests without at least one language value will return a 404 error. Obtain valid language codes from the **Get Languages** endpoint. For multilingual agents, pass all language codes in a single request (e.g. `?language=en-IN&language=hi-IN`). This ensures only models that support every language in the set are returned, preventing incompatible configurations. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Bulk Import FAQs Source: https://docs.gnani.ai/Platform/Import_FAQs POST /v1/agents/{botId}/faqs/import Bulk-import FAQ entries into an agent from a JSON payload **What it does:** Import multiple FAQ entries into an agent in a single request. A successful import automatically sets `faqEnabled = true` on the agent. Entries exceeding the 100-FAQ limit are automatically trimmed — check the `trimmed` field in the response. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Body | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------------------- | | `botId` | string | Yes | Must match the `botId` path parameter. | | `environment` | string | Yes | Target environment: `development`, `staging`, or `production`. | | `faq` | array | Yes | Array of FAQ entry objects to import. | ### FAQ Entry Object | Field | Type | Required | Description | | ----------- | --------- | -------- | ----------------------------------------------------------------------- | | `questions` | string\[] | Yes | Question phrasings. Maximum 10 per entry (extras are silently trimmed). | | `answer` | string | Yes | The spoken answer when a match is detected. | | `language` | string | Yes | BCP-47 language code (e.g. `en-IN`). | | `signal` | string | No | Optional analytics label. | ## Import Limits | Limit | Behaviour | | -------------------------------------- | ----------------------------------------------------------------------------------------- | | Max 100 FAQs per agent per environment | Entries beyond 100 are auto-trimmed. Response includes `trimmed: true` and `trimWarning`. | | Max 10 question phrasings per entry | Entries with more than 10 questions are silently trimmed to 10. | ## Minimum working example ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/faqs/import" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botId": "fb79920229d144608ebf665a10e50275", "environment": "development", "faq": [ { "questions": ["What are your business hours?", "When are you open?"], "answer": "We are open Monday to Friday, 9am to 6pm IST.", "language": "en-IN" }, { "questions": ["How do I cancel my subscription?"], "answer": "To cancel, visit our website or contact billing@example.com.", "language": "en-IN", "signal": "CANCELLATION_INTENT" } ] }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "FAQs imported successfully", "response": { "importedCount": 2, "totalFaqs": 2, "trimmed": false, "trimWarning": null, "faqIds": ["faq_xyz001", "faq_xyz002"] } } ``` ## Response Fields | Field | Type | Description | | --------------- | --------- | ---------------------------------------------------------------------------- | | `importedCount` | integer | Number of FAQ entries successfully imported. | | `totalFaqs` | integer | Total FAQs now stored for this agent in the specified environment. | | `trimmed` | boolean | `true` if some entries were removed because the 100-entry limit was reached. | | `trimWarning` | string | Description of the trim applied. `null` when no trimming occurred. | | `faqIds` | string\[] | Identifiers for all imported FAQ entries. | ## Errors **400 Bad Request** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "FAQ import failed. Invalid data format." } ``` **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Loading a large FAQ dataset into an agent at once * Migrating FAQ content from another system or CMS * Replacing an entire FAQ knowledge base after a content review Always check the `trimmed` and `importedCount` fields in the response. Entries beyond the 100-FAQ limit are silently removed — you will not receive an error, only the warning in the response body. Get a sample payload using the **GET FAQ Sample** endpoint to ensure your JSON is correctly structured before a large import. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # List Agents Source: https://docs.gnani.ai/Platform/List_Agents GET /v1/agents List all agents in your organization and find botId values **What it does:** Returns a paginated list of every voice agent in your organization. Use it to find `botId` values before calling [Get Agent](/Platform/Get_Agent), [Update Agent](/Platform/Update_Agent), or [Trigger Call](/Platform/Trigger_Call). ## Required Permission `agents` ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents?pageNo=1&pageSize=10" \ -H "x-api-key: " ``` ## Additional examples ### Search by name ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents?search=support&pageNo=1&pageSize=10" \ -H "x-api-key: " ``` ## Query parameters | Name | Required | Default | Description | | ---------- | -------- | ------- | ------------------------------------------------------------------- | | `pageNo` | No | `1` | Page number. Must be `1` or higher — `pageNo=0` returns HTTP `500`. | | `pageSize` | No | `10` | Agents per page. | | `search` | No | — | Partial match on agent name. | ## Response ### Success (200) ```json theme={null} { "status": "success", "message": "Agents retrieved successfully", "response": { "pageNo": 1, "pageSize": 10, "totalCount": 25, "totalPages": 3, "bots": [ { "botId": "fb79920229d144608ebf665a10e50275", "botName": "Support Agent", "botInfo": { "botDescription": "Handles inbound customer support calls.", "botLanguage": ["en-IN"], "botRegion": "asia", "botTimezone": "Asia/Kolkata" }, "createdAt": "2026-07-30T16:07:46.636000", "updatedAt": "2026-07-30T16:07:46.636000" } ] }, "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` ### What each field means | Field | What it is | | -------------------------------- | ------------------------------------------------------------------- | | `bots[].botId` | **Copy this.** Use in Get, Update, Trigger Call, and FAQ endpoints. | | `bots[].botName` | Display name shown in the dashboard and conversation logs. | | `bots[].botInfo` | Summary only — language, region, timezone, description. | | `bots[].createdAt` / `updatedAt` | ISO 8601 timestamps. | | `totalCount` / `totalPages` | Use to paginate — increment `pageNo` until it exceeds `totalPages`. | ## List vs Get — which one do I need? | Need | Use List | Use Get | | ------------------------------------- | -------- | ------- | | Find `botId` by name | Yes | No | | Full prompt, ASR, TTS, webhook config | No | Yes | | Check `hasPostCallTrigger` | No | Yes | | Build an agent picker UI | Yes | No | ## Edge cases | Scenario | What happens | | ------------------------ | ----------------------------------- | | `pageNo: 0` | HTTP `500` — always start at `1` | | `search` with no matches | `totalCount: 0`, empty `bots` array | | Large org | Set `pageSize: 100` and loop pages | ## Errors **500 Internal Server Error** - Invalid page number Sending `pageNo=0` (or any value below `1`) returns HTTP `500`. Always start pagination at `pageNo=1`. ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Internal server error" } ``` Use `search` to locate an agent by name instead of paginating through every page manually. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # List FAQs Source: https://docs.gnani.ai/Platform/List_FAQs GET /v1/agents/{botId}/faqs Retrieve all FAQ entries for a specific agent **What it does:** Retrieve all FAQ entries currently stored for the specified agent and environment. Use the returned `faqId` values to update or delete individual entries. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Query Parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | | `environment` | string | No | Filter by environment: `development`, `staging`, or `production`. Default: `development`. | ## Minimum working example ```bash theme={null} curl "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/faqs?environment=development" \ -H "x-api-key: " ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "FAQs retrieved successfully", "response": { "faqs": [ { "faqId": "faq_xyz001", "questions": ["What are your business hours?", "When are you open?"], "answer": "We are open Monday to Friday, 9am to 6pm IST.", "language": "en-IN", "signal": null }, { "faqId": "faq_xyz002", "questions": ["How do I cancel my subscription?"], "answer": "To cancel, visit our website or contact billing@example.com.", "language": "en-IN", "signal": "CANCELLATION_INTENT" } ], "totalCount": 2 } } ``` ## Response Fields | Field | Type | Description | | ------------------ | --------- | -------------------------------------------------------------------- | | `faqs[].faqId` | string | Unique FAQ identifier. Use this when updating or deleting the entry. | | `faqs[].questions` | string\[] | Question phrasings that trigger this FAQ. | | `faqs[].answer` | string | The spoken answer returned when a match is detected. | | `faqs[].language` | string | BCP-47 language code for this entry. | | `faqs[].signal` | string | Optional analytics label. `null` if not set. | | `totalCount` | integer | Total FAQ entries for this agent in the specified environment. | ## Errors **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Reviewing existing FAQ content before making updates * Retrieving `faqId` values needed for **Update FAQ** or **Delete FAQs** * Auditing FAQ coverage across languages and environments Always list FAQs first to get the `faqId` values before calling **Update FAQ** or **Delete FAQs**. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Get Conversation Audio Source: https://docs.gnani.ai/Platform/Stream_Conversation_Audio GET /v1/conversations/{conversationId}/audio Stream or download the audio recording of a specific conversation **What it does:** Retrieve the audio recording of a completed conversation as a binary stream. The response body contains the raw audio data (`audio/mpeg`) which can be streamed directly to an audio player, downloaded as an MP3, or processed programmatically for transcription or archiving. ## Required Permission `conversations` ## Path Parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | -------------------------------------- | | `conversationId` | string | Yes | Unique identifier of the conversation. | ## Minimum working example Download the recording as an MP3 file: ```bash theme={null} curl -H "x-api-key: " \ "https://api.inya.ai/platform/v1/conversations/1786433682702486082/audio" \ --output conversation.mp3 ``` ## Additional examples ### Stream and play (JavaScript) ```javascript theme={null} const response = await fetch( 'https://api.inya.ai/platform/v1/conversations/1786433682702486082/audio', { headers: { 'x-api-key': '' } } ); const blob = await response.blob(); const audioUrl = URL.createObjectURL(blob); const audio = new Audio(audioUrl); audio.play(); ``` ### Stream and save (Python) ```python theme={null} import requests response = requests.get( 'https://api.inya.ai/platform/v1/conversations/1786433682702486082/audio', headers={'x-api-key': ''}, stream=True ) with open('conversation.mp3', 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) ``` ## Response ### Success (200) Returns the audio recording as a binary stream with `Content-Type: audio/mpeg`. The response body is the raw MP3 data. Use **GET** (not `HEAD`) when checking availability — some deployments return `405` for `HEAD` requests. ## Errors **403 Forbidden** — Insufficient permissions ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "User does not have access to this conversation" } ``` **404 Not Found** — Audio recording not available ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Audio file not found at path: /gnanicallshare/dev-.../conversation-id.mp3" } ``` The path in the message is server-specific. A `404` means no recording exists for this conversation. ## Edge cases | Scenario | What happens | | ---------------------------- | ----------------------------------------------------- | | Recording disabled for agent | HTTP `404` | | Call too short to record | HTTP `404` | | Using `HEAD` request | May return HTTP `405` on some deployments — use `GET` | Audio recordings may contain sensitive personal information. Ensure proper access controls are in place and comply with applicable data privacy regulations before storing or sharing recordings. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Trigger Test Call Source: https://docs.gnani.ai/Platform/Trigger_Call POST /v1/agents/{botId}/trigger_call Place an outbound test call from an agent to a whitelisted phone number **What it does:** Tells an agent to call a phone number. The agent runs a live conversation using its current configuration. ```text theme={null} 1. PUT /v1/agents/{botId} → configure agent 2. POST /v1/agents/{botId}/trigger_call → place the call (this page) 3. POST /v1/conversations/logs → find conversationId 4. GET /v1/conversations/{id}/stats → read disposition + transcript ``` Or skip steps 3–4: enable a [post-call webhook](/Platform/Update_Agent#post-call-webhook) and receive call data automatically when the call ends. ## Required Permission `agents` ## Minimum working example Always pass `?environment=development` unless your API key has production access. ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/trigger_call?environment=development" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "phone": "9876543210", "countryCode": "+91", "name": "Jane Doe" }' ``` ## Path and query parameters | Name | Location | Required | Description | | ------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------- | | `botId` | Path | Yes | Agent to place the call from. Get it from [List Agents](/Platform/List_Agents) or [Create Agent](/Platform/Create_Agent). | | `environment` | Query | Often yes | `development`, `staging`, or `production`. Omitting this may return `403` for Developer-role keys. | Outbound calls only work to **whitelisted phone numbers**. Calling an unregistered number returns HTTP `400`. Contact your account manager to register numbers. ## Request body | Field | Required | Description | | ------------------- | -------- | -------------------------------------------------------------------------- | | `phone` | Yes | Phone number **without** country code — e.g. `"9876543210"`. | | `countryCode` | Yes | Dialing code with `+` prefix — e.g. `"+91"`. | | `name` | Yes | Label for the person being called. Appears in conversation logs as `name`. | | `clientReferenceId` | No | Your CRM or campaign ID. Links the call to your system. See below. | ### `clientReferenceId` Optional free-form string to tie the call back to your records (CRM lead ID, booking reference, campaign code). When provided: * Stored on the conversation record * Forwarded to the dynamic greeting API if `hasDynamicGreeting` is enabled * Returned in the trigger response under `response.clientReferenceId` * May appear in post-call webhook payloads When omitted, `response` is `null` on success. ## Response ### Success (200) — with `clientReferenceId` ```json theme={null} { "status": "success", "message": "Call is being triggered to 9876543210", "response": { "clientReferenceId": "crm-lead-42" }, "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` ### Success (200) — without `clientReferenceId` ```json theme={null} { "status": "success", "message": "Call is being triggered to 9876543210", "response": null, "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` A `200` response means the call is **being placed** — not that it has finished. Use [Get Conversation Logs](/Platform/Get_Conversation_Logs) or a post-call webhook to read the outcome after the call ends. ## What happens after you trigger ```text theme={null} Trigger Call (200) │ ▼ Agent dials the number │ ├─ Phone not whitelisted → 400 at trigger time ├─ Dynamic greeting API fails → 400 at trigger time ├─ Callee does not answer → callStatus: "NO ANSWER" in Stats └─ Call connects → utteranceAnalytics + overallCallDisposition in Stats (or post-call webhook fires) ``` ## Edge cases | Scenario | What happens | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Missing `?environment=development` | HTTP `403` — `"Developer can only trigger calls in the development environment"` | | Phone not whitelisted | HTTP `400` — `"Phone number is not registered for outbound calls"` | | Invalid phone format | HTTP `400` — `"Phone number provided is Invalid"` | | Dynamic greeting or pre-call API misconfigured | HTTP `400` — check your [Update Agent](/Platform/Update_Agent) `dynamicInitialMessageConfig` and `preCall` settings | | Agent not accessible | HTTP `403` — wrong `botId` or insufficient permissions | ## Errors **403 Forbidden — missing environment** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Developer can only trigger calls in the development environment" } ``` **400 Bad Request — phone not whitelisted** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Phone number is not registered for outbound calls" } ``` **400 Bad Request — dynamic greeting / pre-call failure** ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Please check your dynamic greeting message API or precall/dynamic variables and try again" } ``` Ensure you have proper consent and comply with local telecommunications regulations before triggering outbound calls. This endpoint is for testing — not large-scale production campaigns. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Update Agent Source: https://docs.gnani.ai/Platform/Update_Agent PUT /v1/agents/{botId} Update an existing agent's configuration — all fields are optional **What it does:** Update an existing voice agent's configuration. All request body fields are optional — only the fields you include are modified. Unspecified fields retain their current values. This endpoint is the primary way to configure an agent's ASR, LLM, TTS, language settings, advance settings, and integrations. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Top-Level Request Body Fields | Field | Type | Description | | ----------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | | `botName` | string | New display name for the agent. | | `description` | string | Updated agent description. | | `greetingMessage` | string | Opening message spoken at the start of each call. | | `endMessage` | string | Closing message spoken before the call ends. | | `systemPrompt` | string | LLM system prompt. Supports Jinja2 template variables (e.g. `{{ customer_name }}`). | | `hasDynamicGreeting` | boolean | Enable fetching the greeting from an external API at call start. | | `dynamicInitialMessageConfig` | object | `{ hasHeaders, method, url, headers[{ id, key, value }] }` — webhook config for dynamic greeting. | | `botDetails` | object | `{ language: string[] (max 3), region, timeZone, description }` | | `asrParams` | object | `{ provider, model, language }` — the selected model must support all configured languages. | | `llmParams` | object | `{ provider, model, temperature (0.0–1.0), maxTokens (1–4096) }` | | `ttsParams` | object | `{ provider, voice, language, ttsRate (0.5–2.0), enableCaching }` | | `advanceSettings` | object | ASR advance settings. See table below. | | `languageSwitch` | boolean | Enable automatic language switching mid-call. | | `explicitLanguageSwitch` | boolean | Use explicit language switch mode (agent asks the caller to confirm). | | `languageSwitchMode` | string | `'implicit'` or `'explicit'`. Controls how language switch is initiated. | | `minWordsForLangSwitch` | integer | Minimum words in the non-primary language before switching. Must be >= 1. Default: `2`. | | `callTransferConfig` | object | `{ description, defaultTransferMessage, details[{ phoneNumber, countryCode, prompt, message }] }` | | `callTransferConfigStatus` | boolean | Enable or disable call transfer functionality. | | `preCall` | object | `{ preCallVariableStatus, preCallParams: { primaryVariable, variables[] } }` | | `enableVoiceMailDetection` | boolean | Enable the voicemail detection feature. | | `voiceMailDetectionConfig` | object | `{ enabled, maxDetectionRetries (>=1), maxVoicemailMessageWaitTime (>=1), voiceMailResponse, postDetectionActions[] }` | | `hasPostCallTrigger` | boolean | Enable or disable the post-call webhook. See [Post-call webhook](#post-call-webhook). | | `postCallTriggerAPIConfig` | object | `{ hasHeaders, method ('GET' \| 'POST'), url, headers[{ id, key, value }] }` — called after every call ends. | ### `advanceSettings` Fields | Field | Type | Range / Notes | | ---------------------------------- | --------- | ---------------------------------------------------------------------------------------- | | `barge` | boolean | Enable or disable caller barge-in. | | `initialMessageBarge` | boolean | Allow the caller to interrupt the opening greeting. | | `useDenoiser` | boolean | Apply background noise filtering. | | `suppressionLevel` | integer | Noise suppression level: 20–100. | | `enableItnForAsr` | boolean | Apply Inverse Text Normalization to ASR output. | | `maxSpeechDuration` | number | Maximum utterance duration in seconds: 1–240. | | `speechInitialSilenceTimeout` | number | Silence threshold before ASR activates: 10–30s (streaming) or 10–60s (others). | | `endSilenceTimeout` | number | End-of-speech silence in milliseconds. Stored directly. | | `speechSegmentationSilenceTimeout` | number | Utterance segmentation silence gap: 0.1–5s. **Streaming models only.** | | `minWordsForBargeIn` | integer | Minimum words before a barge-in registers: 1–5. **Streaming ASR only.** | | `minTimeToBarge` | number | Minimum elapsed time before barge-in is accepted: 0.1–3s. **Non-streaming ASR only.** | | `asrPreemptive` | boolean | Enable Fast Streaming mode. **Fast REST ASR models only.** | | `enableDtmf` | boolean | Accept DTMF keypad input during the call. | | `phraseConfig.phrase` | string\[] | Custom vocabulary hint phrases. Maximum 100 entries. **Vocabulary-capable models only.** | | `phraseConfig.phraseWeight` | number | Recognition bias weight for custom vocabulary: 0.0–2.0. | Sending an `advanceSettings` field that is not supported by the current ASR model returns HTTP 400. Use **Get Transcriber Config** to verify model compatibility before updating. ## ASR Model Compatibility — `advanceSettings` Reference Each `advanceSettings` field is valid only for specific ASR model types. Sending an incompatible field returns HTTP 400. | Setting | Valid For | Supported Models | | --------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `minWordsForBargeIn` | Streaming only | `gnani_asr`, `microsoft_streaming`, `elevenlabs_streaming`, `gladia_streaming`, `gemini-2.5-flash-native-audio-preview-09-2025` | | `minTimeToBarge` | Non-streaming (REST) only | `microsoft_rest`, `elevenlabs_rest` | | `speechSegmentationSilenceTimeout` | Streaming + select REST | `microsoft_streaming`, `elevenlabs_streaming`, `microsoft_rest` | | `phraseConfig.phrase` | Vocabulary-capable models only | `microsoft_streaming`, `gnani_asr`, `elevenlabs_streaming`, `elevenlabs_rest` | | `asrPreemptive` | Fast REST models only | `elevenlabs_rest` | | `speechInitialSilenceTimeout` (max 30s) | Streaming + select REST | `microsoft_streaming`, `elevenlabs_streaming`, `microsoft_rest` | | `speechInitialSilenceTimeout` (max 60s) | Other models | `gnani_asr`, `gladia_streaming`, `gemini-2.5-flash-native-audio-preview-09-2025` | | `barge`, `useDenoiser`, `enableDtmf`, `maxSpeechDuration` | All models | Any | ## Minimum working example ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "greetingMessage": "Hello, how can I help you today?", "endMessage": "Thank you for calling. Have a great day!" }' ``` ## Additional examples ### Full configuration update ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botName": "Support Agent v2", "description": "Handles inbound support queries", "greetingMessage": "Hello, how can I help you today?", "endMessage": "Thank you. Have a great day!", "systemPrompt": "You are a helpful support agent for {{company_name}}.", "hasDynamicGreeting": true, "dynamicInitialMessageConfig": { "hasHeaders": true, "method": "POST", "url": "https://your-server.example.com/bot_initial_message", "headers": [ { "id": "1783576837993", "key": "Authorization", "value": "abc" } ] }, "botDetails": { "language": ["en-IN", "hi-IN"], "region": "asia", "timeZone": "Asia/Kolkata", "description": "Handles support queries" }, "asrParams": { "provider": "gnani", "model": "gnani_vachana_rest", "language": "en-IN" }, "llmParams": { "provider": "gnani", "model": "Pampa Go", "temperature": 0.7, "maxTokens": 400 }, "ttsParams": { "provider": "gnani", "voice": "Pranav", "language": "en-IN", "ttsRate": 1.2, "enableCaching": true }, "advanceSettings": { "barge": true, "initialMessageBarge": false, "useDenoiser": true, "suppressionLevel": 80, "enableItnForAsr": false, "maxSpeechDuration": 15, "speechInitialSilenceTimeout": 10.0, "endSilenceTimeout": 800, "enableDtmf": false }, "languageSwitch": true, "languageSwitchMode": "explicit", "minWordsForLangSwitch": 2, "preCall": { "preCallVariableStatus": true, "preCallParams": { "primaryVariable": "customer_name", "variables": [ { "variable": "customer_name", "sampleValue": "Alice", "description": "Customer name" }, { "variable": "account_id", "sampleValue": "ACC123", "description": "Account ID" } ] } }, "enableVoiceMailDetection": true, "voiceMailDetectionConfig": { "enabled": true, "maxDetectionRetries": 2, "maxVoicemailMessageWaitTime": 3, "voiceMailResponse": "Hi, we tried reaching you. Please call us back.", "postDetectionActions": [] }, "callTransferConfig": { "description": "Transfer to live agent", "defaultTransferMessage": "Please hold, transferring you now.", "details": [ { "phoneNumber": "9876500000", "countryCode": "+91", "prompt": "Transfer", "message": "Hold on" } ] }, "callTransferConfigStatus": false, "hasPostCallTrigger": true, "postCallTriggerAPIConfig": { "hasHeaders": true, "method": "POST", "url": "https://your-server.example.com/call-completed", "headers": [ { "id": "1", "key": "Authorization", "value": "Bearer your-token" } ] } }' ``` `phraseConfig` is omitted above because it is not supported by `gnani_vachana_rest`. Use a vocabulary-capable ASR model (see the compatibility table) if you need custom phrase hints. ### Multilingual agent Set `botDetails.language` to an array of up to three language codes. Use **Get Transcriber Config** and **Get TTS Config** to pick models and voices that support every language in the list. ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botDetails": { "language": ["en-IN", "hi-IN"] }, "asrParams": { "provider": "microsoft", "model": "microsoft_streaming_multilingual", "language": "en-IN" }, "ttsParams": { "provider": "gnani", "voice": "Nalini", "language": "en-IN" }, "languageSwitch": true, "languageSwitchMode": "explicit", "minWordsForLangSwitch": 2 }' ``` ## Post-call webhook Receive an HTTP callback at your URL when a call ends — instead of polling [Get Conversation Logs](/Platform/Get_Conversation_Logs) on a schedule. ### When to use `hasPostCallTrigger` | Approach | Set `hasPostCallTrigger` | Best for | | --------------------- | ------------------------ | -------------------------------------------------------------------------- | | **Post-call webhook** | `true` | CRM updates, payment reminders, SMS/email follow-ups, real-time dashboards | | **Poll Logs + Stats** | `false` (default) | Historical backfills, bulk exports, ad-hoc QA review | **Typical webhook flow:** ```text theme={null} 1. PUT /v1/agents/{botId} → set hasPostCallTrigger: true + your URL 2. Trigger Call → agent places the call 3. Call ends → platform POSTs payload to your URL 4. Your server → update CRM, send follow-up, store transcript ``` Configure `hasPostCallTrigger` and `postCallTriggerAPIConfig` on the same [Update Agent](#top-level-request-body-fields) request. ### Enable the webhook ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "hasPostCallTrigger": true, "postCallTriggerAPIConfig": { "hasHeaders": true, "method": "POST", "url": "https://your-server.example.com/call-completed", "headers": [ { "id": "1", "key": "Authorization", "value": "Bearer your-token" } ] } }' ``` | Field | Description | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `hasPostCallTrigger` | Set `true` to enable. When a call ends, the platform POSTs call data to `postCallTriggerAPIConfig.url`. Set `false` to disable. | | `postCallTriggerAPIConfig.method` | `GET` or `POST`. Use `POST` when sending a JSON body. | | `postCallTriggerAPIConfig.url` | Your HTTPS endpoint. Must be reachable from the platform. | | `postCallTriggerAPIConfig.headers` | Optional auth or tracing headers sent with each callback. | Verify the saved configuration with [Get Agent](/Platform/Get_Agent) — both `hasPostCallTrigger` and `postCallTriggerAPIConfig` are returned on the agent object. ### Expected webhook payload When a call ends, the platform `POST`s a JSON body to your URL. Field names use **snake\_case** (not the camelCase used by the Platform REST APIs). The payload includes call metadata, disposition extraction, the full transcript, and telephony CDR details. Example payload from a verified post-call webhook: ```json theme={null} { "conversation_id": "1786446300918586082", "bot_id": "fbec81f5c97c4a62b0103e87f53c0d56", "bot_name": "Muthoot-sanity_outbound_-dev-c9dfdb3aedbc", "bot_type": "voice_outbound", "bot_language": "en-IN", "bot_languages": ["en-IN"], "environment": "development", "call_type": "live", "flow_id": "dev-c9dfdb3aedbc", "organization_id": "f42bed74-4e0e-4dce-90ce-164982f021b9", "user_id": "29be0f97-51c6-49aa-a515-e0433649e69d", "user_name": "shyam.r@gnani.ai", "phone": "+9109900186082", "phone_number": 9900186082, "user_context": { "phone": "+9109900186082" }, "callStatus": "ANSWERED", "STAGE_CODE": "PTP", "post_call_extraction": "NA", "disposition_result": { "DISPOSITION": "PTP", "ptp_date": "2024-06-21", "clbk_date": "no response", "clbk_time": "no response", "language_captured": "English" }, "post_call_extraction_v2": { "DISPOSITION": "PTP", "ptp_date": "2024-06-21", "clbk_date": "no response", "clbk_time": "no response", "language_captured": "English" }, "transcript": [ { "role": "assistant", "content": "Hello, when will you pay your EMI amount? Please rate our services between 1 and 5.", "timestamp": 1786446300.938049 }, { "role": "user", "content": "actually i don't have money right now can i pay later", "timestamp": 1786446320.126243, "total_results": { "en-IN": "actually i don't have money right now can i pay later" }, "detected_language": "english", "user_interruption_flag": false }, { "role": "assistant", "content": "Alright, That sounds fine. I will note that down. Thank you for your time. Have a good day.", "timestamp": 1786446347.233017 } ], "call_infra": { "call_status": { "callStatus": "ANSWERED", "call_uid": "1786446300918586082", "customerCRTId": "1786446300918586082", "customerPhone": "9900186082", "srcPhone": "+918035218872", "dstPhone": "9900186082|dev-c9dfdb3aedbc;;Shyam R Pole", "callType": "Outbound", "ringingTime": "9", "setupTime": "45", "gnanivb_billing_duration": "45", "callStartTime": "2026/08/11 16:35:01 +0000", "callConnectedTime": "2026/08/11 16:35:09 +0000", "callEndTime": "2026/08/11 16:35:55 +0000", "rec_path": "dev-c9dfdb3aedbc/2026_08_11/dev-c9dfdb3aedbc-1786446300918586082-9900186082-2026_08_11-16_35_01", "flow_id": "dev-c9dfdb3aedbc" } }, "rec_path": "dev-c9dfdb3aedbc/2026_08_11/dev-c9dfdb3aedbc-1786446300918586082-9900186082-2026_08_11-16_35_01", "setupTime": "45", "ringingTime": "9" } ``` `conversation_log` mirrors `transcript` with the same turn-by-turn content. Both arrays are populated in live webhook deliveries — the example above omits duplicate entries for readability. **Webhook fields arrive in snake\_case.** When your endpoint receives the post-call callback, read keys such as `conversation_id`, `bot_id`, `user_context`, and `disposition_result` as snake\_case — not the camelCase used by Platform REST APIs (`conversationId`, `botId`, `userContext`). Transcript user turns follow the same pattern: `detected_language`, `total_results`, `user_interruption_flag`. A few telephony fields (for example `callStatus`, `setupTime`) use camelCase — check the example payload above rather than assuming one convention everywhere. ### Payload field reference | Field | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `conversation_id` | Unique call identifier. Same value as `call_uid` and `customerCRTId`. Use for idempotency and to call [Get Conversation Statistics](/Platform/Get_Conversation_Stats) or [Get Conversation Audio](/Platform/Stream_Conversation_Audio). | | `bot_id` | Agent that handled the call. | | `bot_name` | Display name of the agent (includes environment suffix in development). | | `bot_type` | Agent direction — e.g. `voice_outbound`, `voice_inbound`. | | `environment` | Call environment: `development`, `staging`, or `production`. | | `flow_id` | Internal flow identifier for the agent deployment. | | `user_id` | Platform user identifier for the callee session. | | `user_name` | Session label or callee identifier (e.g. email or name from trigger data). | | `phone` / `phone_number` | Callee phone number in E.164 (`phone`) and numeric (`phone_number`) formats. | | `user_context` | Caller context passed at trigger time (e.g. `{ "phone": "+91..." }`). | | `callStatus` | Telephony outcome at the top level — e.g. `ANSWERED`. Also nested under `call_infra.call_status`. | | `STAGE_CODE` | Short disposition code — see [Disposition codes](/Platform/Get_Conversation_Stats#disposition-codes). | | `disposition_result` | Structured disposition extraction from the agent's post-call prompt. Fields depend on your disposition configuration. | | `post_call_extraction_v2` | Same structured extraction as `disposition_result`. Prefer this object when both are present. | | `transcript` | Full turn-by-turn conversation. User turns include `detected_language`, `total_results`, and `user_interruption_flag`. | | `conversation_log` | Duplicate of `transcript` with the same turn-by-turn content. | | `call_infra.call_status` | Telephony CDR record — connect/end timestamps, ringing and billing duration, source/destination numbers, and recording path. | | `rec_path` | Relative path to the call recording. Also available at `call_infra.call_status.rec_path`. | | `setupTime` / `ringingTime` | Billing and ringing duration in seconds (string). Duplicated from `call_infra.call_status`. | Return HTTP `2xx` from your endpoint quickly and process the payload asynchronously. Key your handler on `conversation_id` to stay idempotent — the same call may be delivered more than once. Pass `clientReferenceId` on [Trigger Call](/Platform/Trigger_Call) to link webhook payloads back to your CRM record. For a Platform-normalized view of the same call, use `conversation_id` with [Get Conversation Statistics](/Platform/Get_Conversation_Stats). ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent updated successfully", "response": { "botId": "fb79920229d144608ebf665a10e50275" } } ``` ## Errors **400 Bad Request** - Validation error ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Selected ASR model does not support selected languages. Please check /platform/v1/agents/config/transcriber_config" } ``` **403 Forbidden** - Insufficient permissions (QA role) ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "QA role does not have permission to create/update agents" } ``` **404 Not Found** - Agent not found or not accessible ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found or does not belong to your organization" } ``` **409 Conflict** - Agent name already taken ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "An agent named 'Support Agent v2' already exists in your organization" } ``` Always validate your ASR and TTS selections against the **Agent Config API** before updating. This ensures the selected provider/model combination supports all configured languages. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Update Chat Widget Config Source: https://docs.gnani.ai/Platform/Update_Chat_SDK_Config PUT /v1/agents/{botId}/chat-sdk-config Update Chat Widget visual customizations and settings for an agent **What it does:** Update the visual customizations and settings for the Chat Widget embedded in your product. All fields are optional — only the fields you include are updated. Unspecified fields retain their current values. Requires the `ORG_ADMIN` or `DEVELOPER` role. The `QA` role does not have write access to widget settings. ## Required Permission `agents` with `ORG_ADMIN` or `DEVELOPER` role ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Body Fields | Field | Type | Description | | ------------------------------------------------------------------ | --------- | ------------------------------------------------------- | | `chatSdkConfig.enabled` | boolean | Enable or disable the Chat Widget. | | `chatSdkConfig.allowedOrigins` | string\[] | Domains permitted to embed the widget (CORS whitelist). | | `chatSdkConfig.windowCustomizations.header.title` | string | Chat window title text. | | `chatSdkConfig.windowCustomizations.header.subtitle` | string | Subtitle text below the title. | | `chatSdkConfig.windowCustomizations.header.logo` | string | URL of the logo shown in the header. | | `chatSdkConfig.windowCustomizations.header.backgroundColor` | string | Header background color (hex, e.g. `#1A73E8`). | | `chatSdkConfig.windowCustomizations.header.textColor` | string | Header text color (hex). | | `chatSdkConfig.windowCustomizations.body.backgroundColor` | string | Chat transcript area background color. | | `chatSdkConfig.windowCustomizations.body.fontSize` | string | Transcript text font size (e.g. `"14px"`). | | `chatSdkConfig.windowCustomizations.body.textColor` | string | Transcript text color. | | `chatSdkConfig.windowCustomizations.userMessage.backgroundColor` | string | User message bubble background color. | | `chatSdkConfig.windowCustomizations.userMessage.fontSize` | string | User message font size. | | `chatSdkConfig.windowCustomizations.userMessage.textColor` | string | User message text color. | | `chatSdkConfig.windowCustomizations.botMessage.backgroundColor` | string | Agent message bubble background color. | | `chatSdkConfig.windowCustomizations.botMessage.fontSize` | string | Agent message font size. | | `chatSdkConfig.windowCustomizations.botMessage.textColor` | string | Agent message text color. | | `chatSdkConfig.windowCustomizations.footer.backgroundColor` | string | Footer background color. | | `chatSdkConfig.windowCustomizations.footer.textColor` | string | Footer text color. | | `chatSdkConfig.windowCustomizations.footer.fontSize` | string | Footer font size. | | `chatSdkConfig.windowCustomizations.footer.infoText` | string | Footer info text (e.g. `"Powered by Gnani"`). | | `chatSdkConfig.windowCustomizations.footer.input.backgroundColor` | string | Text input field background color. | | `chatSdkConfig.windowCustomizations.footer.input.borderColor` | string | Text input field border color. | | `chatSdkConfig.windowCustomizations.footer.input.textColor` | string | Text input field text color. | | `chatSdkConfig.windowCustomizations.footer.input.placeholder` | string | Placeholder text in the input field. | | `chatSdkConfig.windowCustomizations.footer.button.backgroundColor` | string | Send button background color. | | `chatSdkConfig.windowCustomizations.footer.button.textColor` | string | Send button icon/text color. | | `chatSdkConfig.widgetCustomizations.buttonStyles.backgroundColor` | string | Launcher button background color. | | `chatSdkConfig.widgetCustomizations.buttonStyles.width` | string | Launcher button width (e.g. `"60px"`). | | `chatSdkConfig.widgetCustomizations.buttonStyles.height` | string | Launcher button height (e.g. `"60px"`). | ## Minimum working example ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/chat-sdk-config" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "chatSdkConfig": { "enabled": true, "allowedOrigins": ["https://yourapp.example.com"], "windowCustomizations": { "header": { "title": "Support Chat", "subtitle": "How can we help?", "backgroundColor": "#1A73E8", "textColor": "#FFFFFF" }, "body": { "backgroundColor": "#F9FAFB", "fontSize": "14px", "textColor": "#111827" }, "userMessage": { "backgroundColor": "#1A73E8", "textColor": "#FFFFFF", "fontSize": "14px" }, "botMessage": { "backgroundColor": "#E5E7EB", "textColor": "#111827", "fontSize": "14px" }, "footer": { "backgroundColor": "#FFFFFF", "infoText": "Powered by Gnani", "input": { "placeholder": "Type your message..." }, "button": { "backgroundColor": "#1A73E8", "textColor": "#FFFFFF" } } }, "widgetCustomizations": { "buttonStyles": { "backgroundColor": "#1A73E8", "width": "60px", "height": "60px" } } } }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Chat SDK config updated successfully", "response": { "updated": true, "botId": "fb79920229d144608ebf665a10e50275" } } ``` ## Errors **403 Forbidden** - Insufficient role ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "QA role does not have permission to update chat SDK configuration" } ``` **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Matching the chat widget to your brand colors and typography * Customizing the widget for different agents (e.g. support vs. sales) * Enabling or disabling the widget for a specific agent * Restricting widget access to specific domains via `allowedOrigins` Use **Get Chat Widget Config** first to retrieve the current settings before making partial updates. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Update FAQ Source: https://docs.gnani.ai/Platform/Update_FAQ PUT /v1/agents/{botId}/faqs Update a single FAQ entry by its faqId **What it does:** Modify an existing FAQ entry by providing its `faqId`. All supplied fields replace the current values for that entry. Retrieve `faqId` values from the **List FAQs** endpoint. ## Required Permission `agents` ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Body | Field | Type | Required | Description | | --------------- | --------- | -------- | -------------------------------------------------------------- | | `botId` | string | Yes | Must match the `botId` path parameter. | | `environment` | string | Yes | Target environment: `development`, `staging`, or `production`. | | `faq.faqId` | string | Yes | ID of the FAQ entry to update. | | `faq.questions` | string\[] | Yes | Replacement list of question phrasings. Maximum 10 per entry. | | `faq.answer` | string | Yes | Replacement answer text. | | `faq.language` | string | Yes | BCP-47 language code for this entry (e.g. `en-IN`). | ## Minimum working example ```bash theme={null} curl -X PUT "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/faqs" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botId": "fb79920229d144608ebf665a10e50275", "environment": "development", "faq": { "faqId": "faq_xyz001", "questions": ["What are your hours?", "Are you open weekends?"], "answer": "We are open Monday to Saturday, 9am to 6pm IST.", "language": "en-IN" } }' ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "FAQ updated", "response": { "updated": true, "faqId": "faq_xyz001" } } ``` ## Errors **404 Not Found** - Agent not found ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent not found" } ``` ## Use Cases * Correcting an answer after reviewing conversation logs * Adding new question phrasings to improve match rate * Updating content when business information changes Use **List FAQs** to retrieve the `faqId` values before updating. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Upload Chat Widget Icon Source: https://docs.gnani.ai/Platform/Upload_Chat_SDK_Icon POST /v1/agents/{botId}/chat-sdk-icon Upload a custom launcher icon for the Chat Widget **What it does:** Upload a custom image to replace the Chat Widget's floating launcher button icon. The uploaded file is stored and applied to the widget immediately after upload. Requires the `ORG_ADMIN` or `DEVELOPER` role. ## Required Permission `agents` — `ORG_ADMIN` or `DEVELOPER` role ## Path Parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `botId` | string | Yes | Unique agent identifier. | ## Request Send the request as `multipart/form-data` with a single field named `file` containing the image. When using cURL with the `-F` flag, the `Content-Type` header is set automatically. **Accepted formats:** JPEG, PNG, GIF, BMP, WebP, TIFF, SVG ## Minimum working example ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents/fb79920229d144608ebf665a10e50275/chat-sdk-icon" \ -H "x-api-key: " \ -F "file=@/path/to/your/icon.png" ``` ## Response ### Success (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "File uploaded successfully", "response": { "fileUrl": "https://your-storage.blob.core.windows.net/..." } } ``` ## Response Fields | Field | Type | Description | | --------- | ------ | ---------------------------------------------------------------------------------------- | | `fileUrl` | string | URL of the uploaded icon file. Use this URL in your chat widget configuration if needed. | ## Errors | Status | Condition | Message | | ------ | ---------------------------- | ------------------------------------------------------------------------ | | `400` | Unsupported file type | `Unsupported image type. Allowed: JPEG, PNG, GIF, BMP, WebP, TIFF, SVG.` | | `403` | QA role attempted the upload | `QA role does not have permission to update agents.` | | `404` | Agent not found | `Agent not found` | ## Use Cases * Branding the chat widget launcher with your company logo * Using a different icon per agent to reflect different product lines * Updating the icon after a brand refresh For best results, use a square image of at least 60×60 pixels. PNG or WebP with a transparent background renders cleanly on any launcher button color. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Validate Prompt Source: https://docs.gnani.ai/Platform/Validate_Prompt POST /v1/agents/prompt/validate Validate a Jinja2 system prompt template and receive optimization recommendations **What it does:** Validate a system prompt written using **Jinja2** template syntax. The endpoint checks whether all template variables and control flow blocks are syntactically correct, extracts all variable names used in the template, and returns a performance analysis with optimization recommendations. Use this endpoint before saving a new system prompt to an agent via **Update Agent**. ## Required Permission `agents` ## Request Body | Field | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `systemPrompt` | string | Yes\* | The Jinja2 prompt template string to validate. \*Omitting this field or sending `{}` does **not** produce an error — see [Empty Body or Missing `systemPrompt`](#empty-body-or-missing-systemprompt-200) below. | ### Jinja2 Template Syntax The Platform API uses Jinja2 template syntax for dynamic system prompts. Variables are wrapped in double curly braces: ``` You are a helpful assistant. Customer name is {{ customer_name }}. ``` You can also use control blocks: ``` {% for item in order_items %} - {{ item.name }} {% endfor %} ``` At call time, variable values are injected via **Pre-Call Variables** configured on the agent. ## Minimum working example ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents/prompt/validate" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "systemPrompt": "You are a support agent for {{ company_name }}. The customer is {{ customer_name }}. Today is {{ date }}." }' ``` ## Response ### Valid Prompt (200) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Jinja template syntax is valid", "response": { "isValidTemplate": true, "variables": ["company_name", "customer_name", "date"], "analysis": { "scores": { "overall": 85, "kvCache": 90, "rating": "Good" }, "issues": [], "recommendations": [] } } } ``` ### Invalid Prompt (200 with validation error) ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Your prompt is not a valid jinja template. Please check the prompt and try again.", "response": { "isValidTemplate": false, "errors": [ "Unexpected end of template. Jinja was looking for the following tags: 'endfor' or 'else'. The innermost block that needs to be closed is 'for'." ], "variables": [] } } ``` ### Empty Body or Missing `systemPrompt` (200) If the request body is empty (`{}`) or the `systemPrompt` field is absent (e.g. the wrong field name `prompt` is used), the API returns HTTP 200 with `isValidTemplate: true` and empty `variables`. **No error is raised.** Ensure you always send the field named exactly `systemPrompt`. ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Jinja template syntax is valid", "response": { "isValidTemplate": true, "errors": [], "variables": [], "analysis": { "scores": { "overall": 0, "kvCache": 0, "rating": "Unknown" }, "issues": [], "recommendations": [] } } } ``` Sending an empty body or using the wrong field name (e.g. `prompt` instead of `systemPrompt`) does **not** return an error — the API treats the missing prompt as an empty string and returns `isValidTemplate: true` with no variables extracted. Always verify that `variables` in the response matches your expected template variables to confirm the prompt was received correctly. ## Response Fields | Field | Type | Description | | -------------------------- | --------- | ------------------------------------------------------------------------------------------ | | `isValidTemplate` | boolean | Whether the prompt is syntactically valid Jinja2. | | `variables` | string\[] | All template variable names extracted from the prompt (e.g. `"customer_name"`). | | `errors` | string\[] | Array of syntax error messages (empty when valid). | | `analysis` | object | Optimization analysis. Only present when `isValidTemplate` is `true`. | | `analysis.scores.overall` | number | Overall optimization score from 0–100. | | `analysis.scores.kvCache` | number | KV cache efficiency score from 0–100. Higher scores indicate better LLM cache utilization. | | `analysis.scores.rating` | string | Human-readable rating: `Poor`, `Fair`, `Good`, or `Excellent`. | | `analysis.issues` | array | Identified optimization issues. | | `analysis.recommendations` | array | Suggested improvements to the prompt. | ## Errors **401 Unauthorized** - Invalid or missing API key ```json theme={null} { "status": "failure", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Invalid API key" } ``` **Invalid Jinja syntax is not an HTTP error.** The API returns HTTP `200` with `isValidTemplate: false` and an `errors` array — see [Invalid Prompt (200 with validation error)](#invalid-prompt-200-with-validation-error) above. The same applies to an empty body or a missing `systemPrompt` field. See [Error Reference](/Platform/Error_Reference) for other status codes. ## Use Cases * Validating prompts before saving them to an agent * Extracting template variable names to configure Pre-Call Variable mappings * Identifying syntax errors early in the development workflow * Optimizing prompts for better KV cache performance A higher `kvCache` score means the LLM can reuse more of its computation across calls, leading to faster response times and lower costs. Move any content that changes per-call (like customer name) towards the end of the prompt to maximize caching. Run this endpoint as part of your CI/CD pipeline to catch prompt template errors before deployment. Save the `requestId` from every response. You will need it if you contact Gnani support to trace a specific request. # Introduction – Agent Builder Platform API Source: https://docs.gnani.ai/Platform/platform-introduction Create and manage voice agents, FAQs, conversations, and the chat widget over REST. **What it does:** Gives you programmatic control over agents you build in [Agent Builder](/introduction) — create and update configuration, manage FAQs, read conversation logs, and configure the embeddable chat widget. For speech processing in your own stack, use [Gnani Speech APIs](/api/introduction/introduction) instead. ## Base URL ```text theme={null} https://api.inya.ai/platform ``` All paths below are relative to this URL. Requests must use HTTPS. ## Authentication Include your API key on every request: ```bash theme={null} x-api-key: ``` Keys are scoped by permission: | Permission | Endpoints | | --------------- | --------------------- | | `agents` | `/v1/agents/*` | | `conversations` | `/v1/conversations/*` | Invalid keys return `401`. Valid keys without the required permission return `403`. Save the `requestId` from each response. You will need it when contacting support. ## Response format Successful responses use this envelope: ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Human-readable description.", "response": {} } ``` Paginated lists also include `pageNo`, `pageSize`, `totalCount`, and `totalPages` inside `response`. ## API groups | Group | Path | Guide | | ----------------- | ------------------------------------ | ---------------------------------------------------------------------------- | | Agents | `/v1/agents` | [Create Agent](/Platform/Create_Agent) | | Agent config | `/v1/agents/config` | [Get Languages](/Platform/Get_Languages) | | Agent FAQ | `/v1/agents/{botId}/faqs` | [Add FAQs](/Platform/Add_FAQs) | | Conversations | `/v1/conversations` | [Get Conversation Logs](/Platform/Get_Conversation_Logs) | | Post-call webhook | Agent config | [Update Agent — Post-call webhook](/Platform/Update_Agent#post-call-webhook) | | Chat SDK | `/v1/agents/{botId}/chat-sdk-config` | [Get Chat SDK Config](/Platform/Get_Chat_SDK_Config) | ## Typical integration flows ### Build and deploy an agent ```text theme={null} Get Languages + Transcriber Config → pick ASR/TTS for your languages Create Agent → get botId Update Agent → prompt, voice, webhook, pre-call vars Validate Prompt → check Jinja2 before saving Trigger Call → test with a whitelisted number Get Conversation Logs → Stats → read disposition + transcript ``` 1. [Get Languages](/Platform/Get_Languages) and [Get Transcriber Config](/Platform/Get_Transcriber_Config) 2. [Create Agent](/Platform/Create_Agent) — get a `botId` 3. [Update Agent](/Platform/Update_Agent) — set prompt, voice, and integrations 4. [Trigger Call](/Platform/Trigger_Call) — test in `development` 5. [Get Conversation Logs](/Platform/Get_Conversation_Logs) → [Get Conversation Statistics](/Platform/Get_Conversation_Stats) ### Review calls after they happen **Option A — Poll (good for backfills and ad-hoc review)** 1. [Get Conversation Logs](/Platform/Get_Conversation_Logs) — list calls; copy `conversationId` 2. [Get Conversation Statistics](/Platform/Get_Conversation_Stats) — read `overallCallDisposition` and `utteranceAnalytics` 3. [Get Conversation Audio](/Platform/Stream_Conversation_Audio) — download MP3 (optional) **Option B — Webhook (good for real-time CRM integrations)** 1. [Update Agent](/Platform/Update_Agent#post-call-webhook) — set `hasPostCallTrigger: true` and your URL 2. [Trigger Call](/Platform/Trigger_Call) — place the call 3. Your server receives the post-call payload automatically when the call ends ## Understanding call outcome fields When you read call data — from Stats, Logs, or a webhook — you will see several outcome fields. Here is what each one means: | Field | Where | Meaning | | ----------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `callStatus` | Stats API (camelCase) | Telephony — did the phone connect? (`ANSWERED`, `NO ANSWER`) | | `overallCallDisposition` | Stats API | Short business code — e.g. `PTP`, `RNR`, `DSCN`. See [Disposition codes](/Platform/Get_Conversation_Stats#disposition-codes). | | `callSummary.disposition` | Stats API | Optional LLM label in plain language — may be `null` | | `utteranceAnalytics` | Stats API | Full turn-by-turn transcript | | `STAGE_CODE` / `disposition_result` | Post-call webhook (snake\_case) | Same business outcome as `overallCallDisposition` | See [Get Conversation Statistics](/Platform/Get_Conversation_Stats#the-three-fields-developers-ask-about) for a full breakdown with examples. ## Quick start Create an agent with one request: ```bash theme={null} curl -X POST "https://api.inya.ai/platform/v1/agents" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "botName": "Support Agent", "description": "Handles inbound customer support calls.", "region": "asia", "timeZone": "Asia/Kolkata" }' ``` Example response: ```json theme={null} { "status": "success", "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message": "Agent created successfully.", "response": { "botId": "fb79920229d144608ebf665a10e50275", "botName": "Support Agent" } } ``` Use the returned `botId` to configure the agent, attach FAQs, and trigger calls. ### Try the API in Postman Prefer Postman to curl? Download the collection below — every Platform API endpoint in one place, with headers and sample bodies ready to go. Import it, add your API key, and start sending requests. **Download the Postman collection** (43 KB) Right-click the link and choose **Save link as…** if your browser opens the JSON instead of downloading. ### Import into Postman Once you have the file: 1. Open [Postman](https://www.postman.com/downloads/) 2. Click **Import** → **Upload Files** → select the downloaded JSON 3. Use **Find & Replace** (Cmd+Shift+F / Ctrl+Shift+H) on the collection to replace placeholders: `YOUR_PLATFORM_API_KEY`, `YOUR_BOT_ID`, `YOUR_CONVERSATION_ID`, `YOUR_FAQ_ID` 4. Open any request and click **Send** — no separate environment file is required ## Concepts **System prompts** — Use Jinja2 variables such as `{{ customer_name }}`. Validate syntax with [Validate Prompt](/Platform/Validate_Prompt) before saving. **Multi-language agents** — Up to three languages per agent. Call [Get Transcriber Config](/Platform/Get_Transcriber_Config) and [Get TTS Config](/Platform/Get_TTS_Config) before [Update Agent](/Platform/Update_Agent). **Pre-call variables** — Inject caller-specific fields into the prompt at call time. **FAQ knowledge base** — Up to 100 Q\&A pairs per agent; matched before the LLM generates an answer. **Outbound calls** — Use [Trigger Call](/Platform/Trigger_Call) with `clientReferenceId` to link calls to your CRM. **Post-call automation** — Set `hasPostCallTrigger: true` via [Update Agent](/Platform/Update_Agent#post-call-webhook) to push call outcomes to your server. Use polling (Logs → Stats) when you need historical data instead. ## HTTP status codes | Code | Meaning | | ---- | ----------------------------------------- | | 200 | Success | | 201 | Created | | 400 | Invalid request | | 401 | Authentication failed | | 403 | Permission denied or inaccessible `botId` | | 404 | Resource not found | | 409 | Conflict (e.g. duplicate agent name) | | 429 | Rate limit exceeded | | 500 | Server error | ## Best practices * Call the Config API before creating or updating agents so ASR and TTS choices support every configured language. * Validate prompts before saving. * Test in development first; pass `?environment=development` when required. * Log every `requestId` for support escalations. * Keep API keys out of source control. ## Next steps * [Try the API in Postman](/Platform/platform-introduction#download) * [Create Agent](/Platform/Create_Agent) * [Get Languages](/Platform/Get_Languages) * [Validate Prompt](/Platform/Validate_Prompt) * [Get Conversation Logs](/Platform/Get_Conversation_Logs) * [Update Agent — Post-call webhook](/Platform/Update_Agent#post-call-webhook) * [Error Reference](/Platform/Error_Reference) *** ## Docs feedback Docs feedback: [Agent Builder Platform docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571) **Last verified:** 11 August 2026 # Cancel Job Source: https://docs.gnani.ai/api/STT/batch/cancel-job POST /stt/v3/batch/jobs/{job_id}/cancel Cancel a batch transcription job. ## Overview Cancel a batch job that is not already in a conflicting terminal state. | Current status | Result | | ------------------------------------- | ------------------------------- | | `CREATED` | Immediately `CANCELLED` (`200`) | | `STARTING` / `QUEUED` / `IN_PROGRESS` | `CANCELLING` → `CANCELLED` | | Already `CANCELLED` | `409 JOB_CANCELLED` | | Other terminal states | Conflict error | ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters Job ID returned by **Create Job**. ## Request Body Optional cancel reason. ## Example cURL ```bash theme={null} curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs/019fa262-971e-766b-95f0-ea57d8e0626d/cancel" \ -H "X-API-Key-ID: " \ -H "Content-Type: application/json" \ --data-raw '{"reason":"No longer needed"}' ``` ## Response ### Success (200) ```json theme={null} { "job_id": "019fa262-971e-766b-95f0-ea57d8e0626d", "status": "CANCELLED", "message": "Job cancelled successfully.", "cancelled_at": "2026-07-27T07:03:18.827605Z" } ``` ### Error responses **409 Already cancelled** ```json theme={null} { "error": "JOB_CANCELLED", "message": "Job is already cancelled" } ``` ## Use Cases * Cancelling a job before start when files were uploaded by mistake * Stopping an in-progress batch when the pipeline is aborted # Create Job Source: https://docs.gnani.ai/api/STT/batch/create-job POST /stt/v3/batch/jobs Create a new batch transcription job with uploaded files, a ZIP, or public URLs. ## Overview Create a new batch transcription job within your organization. The job is created in `CREATED` status with your audio accepted — it does **not** start transcription yet. You can then start processing via the **Start Job** endpoint. | Method | Best for | Content-Type | | ------------------- | ---------------------------------------- | --------------------- | | Direct upload / ZIP | Files on your machine or CI | `multipart/form-data` | | Public URL | HTTPS links, including S3 presigned URLs | `application/json` | Authenticated private bucket access is **not** supported. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Request Body ### Multipart upload JSON string with STT configuration. Required fields: `model`, `language_code`. Optional: `mode`, `with_diarization`, `num_speakers`, `is_multi_channel`. Audio file or ZIP archive. Repeat this field for multiple files. Max **100** files per job, **10 MB** per file. ZIP up to **50 MB** compressed. Optional HTTPS webhook URL. When set, Gnani POSTs terminal job results (including `full_transcript`) to this URL. ### Public URL (JSON) | Field | Type | Required | Description | | ------------------ | --------- | -------- | ----------------- | | `config` | object | Yes | STT config object | | `source.type` | string | Yes | `"cloud_storage"` | | `source.auth.mode` | string | Yes | `"public"` | | `source.paths` | string\[] | Yes | Public HTTPS URLs | | `callback_url` | string | No | Webhook URL | ## Example cURL ```bash theme={null} curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \ -H "X-API-Key-ID: " \ -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":false,"is_multi_channel":false};type=application/json' \ -F "files=@./customer_call.wav" ``` ## Response ### Success (201) ```json theme={null} { "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "status": "CREATED", "total_files_accepted": 1, "created_at": "2026-07-27T06:53:34.750193Z", "message": "1 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing." } ``` Save `job_id`. Always start with `/stt/v3/batch/jobs/{job_id}/start` (even if the message mentions `/v1/jobs/...`). ### Error responses **400 Unsupported language** ```json theme={null} { "error": "UNSUPPORTED_LANGUAGE", "message": "Value error, This service does not support language_code 'xx-XX'. Supported languages: bn-BD, bn-IN, en-IN, hi-IN, kn-IN, ml-IN, mr-IN, ta-IN, te-IN" } ``` **401 Missing API key** ```json theme={null} { "detail": { "error_code": "MISSING_API_KEY", "message": "Missing API key", "status_code": 401 } } ``` ## Use Cases * Uploading one or many call recordings for offline transcription * Sending a ZIP of customer audio for batch processing * Pointing to public HTTPS / S3 presigned URLs without re-uploading files Use the **Try it** playground to run this request with your API key. Then call **Start Job**. # Get Job Files Source: https://docs.gnani.ai/api/STT/batch/get-files GET /stt/v3/batch/jobs/{job_id}/files List per-file results and transcript URLs for a batch job. ## Overview List per-file results for a job. For completed files, this returns `transcript_url` — **not** the transcript text. To get `full_transcript` without webhooks: 1. Call this endpoint after the job is terminal (filter with `status=COMPLETED`) 2. `GET` each `transcript_url` (no API key — pre-signed URL) 3. Read `full_transcript` from the JSON `transcript_url` expires in **1 hour**. Re-call this endpoint for a fresh URL. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters Job ID returned by **Create Job**. ## Query Parameters Filter by file status. Use `COMPLETED` to fetch successful transcripts. Page size (1–100). Pagination cursor from `pagination.next_cursor`. ## Example cURL ```bash theme={null} curl "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6/files?status=COMPLETED" \ -H "X-API-Key-ID: " ``` Download transcript text: ```bash theme={null} curl -L "" ``` ## Response ### Success (200) ```json theme={null} { "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "data": [ { "file_id": "28255118-03ca-48c5-807e-8d07a6f54d54", "original_path": "customer_call.wav", "status": "COMPLETED", "duration_seconds": "6.01", "transcript_url": "https://gnaniasraudios.s3.amazonaws.com/results/example.json?X-Amz-Expires=3600", "error_message": null, "created_at": "2026-07-27T06:53:34.750193Z", "completed_at": "2026-07-27T06:53:52.128197Z" } ], "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } ``` ### transcript\_url JSON ```json theme={null} { "file_id": "28255118-03ca-48c5-807e-8d07a6f54d54", "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "original_path": "customer_call.wav", "language_code": "en-IN", "duration_seconds": 6.01, "model": "gnani-prisma-v2.5", "mode": "transcribe", "segments": [ { "segment_id": 0, "start_time": 0.0, "end_time": 0.71, "text": "hello", "speaker_id": 1 } ], "full_transcript": "hello how can i help you today i am calling about my recent order number 12345", "created_at": "2026-07-27T06:53:52Z" } ``` ## Use Cases * Fetching transcript download links after a job completes * Inspecting failed / skipped files via `error_message` # Get Job Status Source: https://docs.gnani.ai/api/STT/batch/get-job GET /stt/v3/batch/jobs/{job_id} Get batch job status and progress counters. ## Overview Get the current status and progress of a batch job. This response does **not** include transcript text. After a terminal status, call **Get Job Files**, then `GET` each `transcript_url`. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters Job ID returned by **Create Job**. ## Polling guidance | Status | Terminal | Next step | | ----------------- | -------- | --------------------------------------------- | | `COMPLETED` | Yes | Call `/files`, then GET each `transcript_url` | | `PARTIAL_FAILURE` | Yes | Same — fetch successful files via `/files` | | `FAILED` | Yes | Inspect `/files` + `error_message` | | `START_FAILED` | Yes | Read `cancel_reason` | | `CANCELLED` | Yes | Done | Poll no faster than every **10 seconds**. For 100+ file jobs, prefer every **30 seconds**. ## Example cURL ```bash theme={null} curl "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6" \ -H "X-API-Key-ID: " ``` ## Response ### Success (200) ```json theme={null} { "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "status": "COMPLETED", "config": { "model": "gnani-prisma-v2.5", "mode": "transcribe", "language_code": "en-IN", "with_diarization": false, "num_speakers": null, "is_multi_channel": false }, "progress": { "total_files": 1, "completed_files": 1, "failed_files": 0, "in_progress_files": 0, "queued_files": 0, "cancelled_files": 0 }, "cancel_reason": null, "created_at": "2026-07-27T06:53:34.750193Z", "started_at": "2026-07-27T06:53:37.276061Z", "completed_at": "2026-07-27T06:53:52.179794Z", "cancelled_at": null, "updated_at": "2026-07-27T06:53:52.179928Z" } ``` ### Error responses **404 Job not found** ```json theme={null} { "error": "JOB_NOT_FOUND", "message": "Job 01900000-0000-7000-8000-000000000000 not found" } ``` ## Use Cases * Polling until a job reaches a terminal status * Checking progress counters for multi-file jobs Next: call **Get Job Files** with `status=COMPLETED`. # List Jobs Source: https://docs.gnani.ai/api/STT/batch/list-jobs GET /stt/v3/batch/jobs List batch transcription jobs with optional status filter and pagination. ## Overview List batch jobs for your organization. Use filters and cursor pagination to browse history. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Query Parameters Filter by job status (`CREATED`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, etc.). Page size (1–100). Pagination cursor from `pagination.next_cursor`. ## Example cURL ```bash theme={null} curl "https://api.vachana.ai/stt/v3/batch/jobs?limit=5" \ -H "X-API-Key-ID: " ``` ## Response ### Success (200) ```json theme={null} { "data": [ { "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "status": "COMPLETED", "total_files": 1, "created_at": "2026-07-27T06:53:34.750193Z", "completed_at": "2026-07-27T06:53:52.179794Z" } ], "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } ``` ## Use Cases * Reviewing recent batch jobs in your org * Filtering failed or in-progress jobs for ops dashboards # Start Job Source: https://docs.gnani.ai/api/STT/batch/start-job POST /stt/v3/batch/jobs/{job_id}/start Start processing a batch transcription job. ## Overview Start transcription for a job in `CREATED` status. Returns `202 Accepted` while validation and queueing run in the background. Creating a job does **not** start processing — you must call this endpoint. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters Job ID returned by **Create Job**. ## Example cURL ```bash theme={null} curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6/start" \ -H "X-API-Key-ID: " ``` ## Response ### Success (202) ```json theme={null} { "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6", "status": "STARTING", "message": "Job start accepted. File validation and queueing are running in the background. Poll GET /stt/v3/batch/jobs/{job_id} to track progress." } ``` ### Error responses **409 Already started** ```json theme={null} { "error": "JOB_ALREADY_STARTED", "message": "Job cannot be restarted from state 'COMPLETED'" } ``` ## Use Cases * Kicking off transcription after files are uploaded * Starting a public-URL job after create (URL validation happens at start) Next: poll **Get Job Status** every 10 seconds (30 seconds for 100+ file jobs) until a terminal status. # Create Batch Job Test Source: https://docs.gnani.ai/api/STT/create-batch-test POST /stt/v3/batchjobs OpenAPI binding test for create. ## Overview Binding test. # Speech-to-Text (REST) Source: https://docs.gnani.ai/api/STT/speech-to-text POST /stt/v3 Quick transcription of audio clips up to 60 seconds via HTTP. ## Overview The REST endpoint transcribes an audio file in a single synchronous HTTP request and returns the transcript immediately. It is best suited for short, pre-recorded audio clips. | Use case | Recommended endpoint | | --------------------------------- | -------------------------------------------------- | | Short clips ≤ 60 s (ideal ≤ 30 s) | **This endpoint** | | Live microphone / real-time audio | [STT Realtime (WebSocket)](/api/STT/stt-websocket) | | Large files or bulk jobs | [STT Batch](/api/STTBatch/Introduction) | *** ## Endpoint ```text theme={null} POST https://api.vachana.ai/stt/v3 Content-Type: multipart/form-data ``` *** ## Authentication Pass your API key in the request header. | Header | Type | Required | Description | | -------------- | -------- | -------- | ------------------------------------------------------------------------- | | `X-API-Key-ID` | `string` | Yes | Your Gnani Prisma v2.5 API key. Obtain one from the Gnani APIs dashboard. | *** ## Request Parameters All parameters are sent as `multipart/form-data` fields. Audio file to transcribe. Supported formats: WAV, MP3, OGG, FLAC, AAC, M4A. Maximum duration: 60 seconds (ideal ≤ 30 s). BCP-47 language code. See [Supported Languages](#supported-languages) below. `verbatim` — raw spoken-form output. `transcribe` — enables Inverse Text Normalization (ITN): numbers, currency, dates, and phone numbers are written in their conventional form. See [ITN](#inverse-text-normalization-itn) below. When `format=transcribe`, set `true` to render digits in the native script of the target language (e.g. `₹५,०००` instead of `₹5,000` for Hindi). Has no effect when `format=verbatim`. *** ## Response ### 200 — Success ```json theme={null} { "success": true, "request_id": "req_abc123", "timestamp": "20251226_143052.123", "transcript": "नमस्ते, आप कैसे हैं?" } ``` | Field | Type | Description | | ------------ | --------- | --------------------------------------------------------------------------------------- | | `success` | `boolean` | `true` when transcription completed without error. | | `request_id` | `string` | Unique identifier for this request. Use it when contacting support or correlating logs. | | `timestamp` | `string` | Server-side request timestamp in `YYYYMMDD_HHMMSS.mmm` format. | | `transcript` | `string` | The transcribed text. Format depends on the `format` parameter. | ### Error Responses | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | `400` | Bad request — invalid parameters or unsupported audio format. | | `429` | Rate limit exceeded — slow down or contact support to increase limits. | | `500` | Internal server error — transient issue on our side; retry with backoff. | | `503` | Service unavailable — the STT service is temporarily down. | *** ## Code Example ```bash cURL theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3 \ --header 'Content-Type: multipart/form-data' \ --header 'X-API-Key-ID: ' \ --form audio_file='@recording.wav' \ --form language_code=hi-IN \ --form format=transcribe \ --form itn_native_numerals=true ``` ```python Python SDK theme={null} from gnani.stt import GnaniSTTClient client = GnaniSTTClient(api_key="your-api-key") result = client.transcribe("recording.wav", language_code="hi-IN") print(result["transcript"]) ``` *** ## Python SDK The official Python SDK handles multipart construction, authentication headers, and retries automatically. ### Installation ```bash theme={null} pip install gnani-vachana ``` Requires **Python 3.10+**. ### Authentication The client requires an `api_key` credential. You can pass it directly or load it from environment variables. ```python Constructor arguments theme={null} from gnani.stt import GnaniSTTClient client = GnaniSTTClient( api_key="your-api-key" ) ``` ```bash Environment variables theme={null} export GNANI_API_KEY="your-api-key" ``` ```python Environment variables (usage) theme={null} from gnani.stt import GnaniSTTClient # Picks up credentials from environment automatically client = GnaniSTTClient() ``` ### Transcribe Audio ```python From a file path theme={null} result = client.transcribe("recording.wav", language_code="hi-IN") print(result["transcript"]) ``` ```python From a file object theme={null} with open("recording.wav", "rb") as f: result = client.transcribe(f, language_code="hi-IN") print(result["transcript"]) ``` ```python From raw bytes theme={null} with open("recording.wav", "rb") as f: audio_bytes = f.read() result = client.transcribe(audio_bytes, language_code="hi-IN") print(result["transcript"]) ``` ### Custom Request ID Pass a `request_id` to correlate SDK calls with your own logs or support tickets. ```python theme={null} result = client.transcribe( "call.flac", language_code="hi-IN", request_id="my-trace-123", ) ``` ### Error Handling ```python theme={null} from gnani.stt import ( AuthenticationError, InvalidAudioError, APIError, ) try: result = client.transcribe("audio.wav", language_code="hi-IN") print(result["transcript"]) except AuthenticationError: print("Invalid credentials — check your organization_id, api_key, and user_id.") except InvalidAudioError as e: print(f"Bad audio file: {e}") except APIError as e: print(f"API error {e.status_code}: {e}") ``` *** ## Supported Languages The Gnani Prisma v2.5 API supports 10 Indian languages. | Language | Code | Native Script | Example | | --------- | ------- | ---------------------------------- | ------------------------------- | | Bengali | `bn-IN` | Bengali (বাংলা) | "আমি ভাত খাই" | | English | `en-IN` | English (Latin) | "I am going to the market" | | Gujarati | `gu-IN` | Gujarati (ગુજરાતી) | "હું બજાર જાઉં છું" | | Hindi | `hi-IN` | Devanagari (हिन्दी) | "मैं बाज़ार जा रहा हूँ" | | Kannada | `kn-IN` | Kannada (ಕನ್ನಡ) | "ನಾನು ಮಾರುಕಟ್ಟೆಗೆ ಹೋಗುತ್ತೇನೆ" | | Malayalam | `ml-IN` | Malayalam (മലയാളം) | "ഞാൻ ചന്തയിലേക്ക് പോകുന്നു" | | Marathi | `mr-IN` | Devanagari (मराठी) | "मी बाजारात जातोय" | | Punjabi | `pa-IN` | Gurmukhi (ਪੰਜਾਬੀ) | "ਮੈਂ ਬਾਜ਼ਾਰ ਜਾ ਰਿਹਾ ਹਾਂ" | | Tamil | `ta-IN` | Tamil (தமிழ்) | "நான் சந்தைக்கு செல்கிறேன்" | | Telugu | `te-IN` | Telugu (తెలుగు) | "నేను మార్కెట్‌కి వెళ్తున్నాను" | **Batch STT:** Gujarati (`gu-IN`) and Punjabi (`pa-IN`) are supported here on REST but **not** on [Batch STT](/api/STTBatch/Introduction). Use REST or Realtime for those languages; use Batch for the nine languages listed on the Batch introduction page. *** ## Inverse Text Normalization (ITN) ITN converts the spoken-form output of the ASR engine into the conventional written form a reader expects — numbers become digits, currency gets the ₹ symbol, dates are formatted, and phone numbers are compacted — all in one pass, immediately after transcription. **How to enable:** Set `format=transcribe` in the request body. ### What ITN Normalizes #### 1 — Cardinal & Ordinal Numbers Whole numbers and positional ranks are formatted using Indian comma grouping (groups of 2 after the first 3 digits). | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------- | -------------------- | ----------------------- | | दो हज़ार | 2,000 | Indian comma grouping | | पाँच लाख बीस हज़ार | 5,20,000 | Lakh-scale grouping | | उन्नीस सौ चौरानवे | 1,994 | Hundred-base year form | | five lakh | 5,00,000 | English lakh convention | | पहला / twenty first | 1st / 21st | Ordinal suffix | #### 2 — Currency & Money All Indian currency expressions — including paise fractions and lakh/crore scales — are formatted with the ₹ symbol and Indian comma grouping. | Spoken input (ASR) | Written output (ITN) | Rule | | --------------------------- | -------------------- | ---------------------- | | पाँच सौ रुपये | ₹500 | ₹ + amount | | तीन रुपये पचास पैसे | ₹3.50 | ₹ + rupees.paise | | दस लाख रुपये | ₹10,00,000 | ₹ + lakh grouping | | I need five thousand rupees | ₹5,000 | English India pipeline | #### 3 — Dates | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------------------------ | -------------------- | ----------------------- | | बीस जनवरी दो हज़ार पच्चीस | 20 जनवरी 2025 | DD Month YYYY (hi) | | fifteenth january twenty twenty five | 15th January 2025 | Ordinal Month YYYY (en) | #### 4 — Times Indian time-of-day words (सुबह, दोपहर, शाम, रात) automatically map to 24-hour HH:MM output. | Spoken input (ASR) | Written output (ITN) | Rule | | -------------------------------------- | ---------------------------- | ----------------------- | | सुबह पाँच बजे | सुबह 05:00 | सुबह = AM | | शाम पाँच बजे | शाम 17:00 | शाम = evening (16–20 h) | | रात के दस बजे | रात 22:00 | रात = night (20–24 h) | | meeting at five fifteen in the evening | meeting 17:15 in the evening | en — 24-hour | #### 5 — Phone Numbers & PIN Codes Digit streams are concatenated into compact numeric strings. 10-digit streams → mobile number; 6-digit streams → PIN. Repeat prefixes (double/डबल, triple/ट्रिपल) are expanded. | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------------------------- | -------------------- | ------------------- | | नौ आठ सात छह पाँच चार तीन दो एक शून्य | 9876543210 | 10 digits → phone | | एक एक शून्य शून्य शून्य एक | 110001 | 6 digits → PIN | | डबल आठ नौ शून्य एक दो तीन चार पाँच छह | 8890123456 | double prefix | | one two three four five six | 123456 | English digit words | #### 6 — Mixed & Code-Mixed Utterances A single sentence may contain multiple entity types or blend Hindi and English. ITN handles all in one pass, normalizing each entity independently. | Spoken input (ASR) | Written output (ITN) | | ----------------------------------------------------- | --------------------------------- | | कल थ्री फिफ्टी पीएम को पाँच सौ रुपये transfer करना है | कल 15:50 को ₹500 transfer करना है | ### Native Script Digits — `itn_native_numerals` By default, ITN outputs Western Arabic digits (0–9) regardless of language. Set `itn_native_numerals=true` to render digits in the native script of the target language. | Language | Spoken input | `false` (default) | `true` — native script | | --------------- | -------------------- | ----------------- | -------------------------- | | Hindi `hi-IN` | पाँच हज़ार रुपये | ₹5,000 | ₹५,००० | | English `en-IN` | five thousand rupees | ₹5,000 | ₹5,000 (Latin — no change) | ### What ITN Does Not Change ITN intentionally preserves idiomatic and ambiguous phrases to avoid incorrect normalization. * **दो तीन** (meaning *a few*) stays as text, not `2` or `3` * **कर दो / ले दो** (imperative verbs) are kept as words, not treated as cardinal 2 If a word or phrase is unchanged in the output, treat it as a failure only when the input was unambiguously a numeric entity. # Start Batch Job Test Source: https://docs.gnani.ai/api/STT/start-batch-test POST /stt/v3/batchjobs/{job_id}/start OpenAPI binding test for start. ## Overview Binding test. # Batch STT Introduction Source: https://docs.gnani.ai/api/STT/stt-batch Asynchronous transcription of long or multiple audio files via the Batch Jobs API. Use **STT Batch** to transcribe long recordings or many files asynchronously. **Flow:** create a job → start it → poll until it finishes → download transcripts\ (or receive them on a webhook). | If you need… | Use | | -------------------------------------------------- | -------------------------------------- | | Long files, many files, or offline pipelines | **STT Batch** (this section) | | Short clips (under \~60 seconds), immediate answer | [STT REST](/api/STT/speech-to-text) | | Live microphone / streaming audio | [STT Realtime](/api/STT/stt-websocket) | *** ## How it works ```text theme={null} 1. CREATE POST /stt/v3/batch/jobs → 201 job_id status: CREATED 2. START POST /stt/v3/batch/jobs/{job_id}/start → 202 status: STARTING 3. WAIT GET /stt/v3/batch/jobs/{job_id} poll every 10s until a terminal status 4. FILES GET /stt/v3/batch/jobs/{job_id}/files → transcript_url for each completed file 5. DOWNLOAD GET → JSON with full_transcript + segments ``` **Important:** creating a job does **not** start transcription. You must call `/start`. *** ## Interactive API reference Each endpoint below has a **Try it** playground (same pattern as Platform Guides): | Step | Endpoint | Page | | ---- | ----------------------------------------- | ----------------------------------------- | | 1 | `POST /stt/v3/batch/jobs` | [Create Job](/api/STT/batch/create-job) | | 2 | `POST /stt/v3/batch/jobs/{job_id}/start` | [Start Job](/api/STT/batch/start-job) | | 3 | `GET /stt/v3/batch/jobs/{job_id}` | [Get Job Status](/api/STT/batch/get-job) | | 4 | `GET /stt/v3/batch/jobs/{job_id}/files` | [Get Job Files](/api/STT/batch/get-files) | | — | `GET /stt/v3/batch/jobs` | [List Jobs](/api/STT/batch/list-jobs) | | — | `POST /stt/v3/batch/jobs/{job_id}/cancel` | [Cancel Job](/api/STT/batch/cancel-job) | Paste your `X-API-Key-ID` in the playground and run live requests from the docs. *** ## Before you begin 1. An API key from the Gnani APIs dashboard 2. Audio in a [supported format](#supported-audio-formats) 3. A [supported language code](#supported-languages) **Base URL** ```text theme={null} https://api.vachana.ai ``` **Auth** — send on every Batch API request: ```http theme={null} X-API-Key-ID: ``` *** ## How do I get the transcript text? | Option | When to use | What you do | | ------------------------ | ------------------------- | ------------------------------------------------------------------------------------- | | **A. Polling (default)** | No webhook | Poll job status → `GET /files` → `GET` each `transcript_url` → read `full_transcript` | | **B. Webhook** | You passed `callback_url` | Receive a POST with `transcripts[]` including `full_transcript` | `GET /jobs/{job_id}` and `GET /jobs/{job_id}/files` do **not** return `full_transcript`. *** ## Configuration | Field | Type | Required | Default | Description | | ------------------ | ------- | ------------------------- | -------------- | ----------------------------------------------- | | `model` | string | Yes | — | `"gnani-prisma-v2.5"` | | `language_code` | string | Yes | — | See [Supported languages](#supported-languages) | | `mode` | string | No | `"transcribe"` | Use `"transcribe"` for now | | `with_diarization` | boolean | No | `false` | Enable speaker diarization | | `num_speakers` | integer | Yes, if diarization is on | — | Max **2** | | `is_multi_channel` | boolean | No | `false` | `true` only for true multi-channel audio | Inverse Text Normalization (ITN) is **not** supported for STT Batch. *** ## Limits | Limit | Value | | ------------------------- | ------------------------------------------- | | Max audio files per job | **100** | | Max file size | **10 MB** per file | | Max ZIP compressed size | **50 MB** | | Max ZIP decompressed size | **200 MB** | | `transcript_url` expiry | **1 hour** | | Poll interval | **≥ 10 s** (prefer **30 s** for 100+ files) | *** ## Supported languages | Code | Language | | ------- | -------------------- | | `bn-BD` | Bengali (Bangladesh) | | `bn-IN` | Bengali (India) | | `en-IN` | English (India) | | `hi-IN` | Hindi | | `kn-IN` | Kannada | | `ml-IN` | Malayalam | | `mr-IN` | Marathi | | `ta-IN` | Tamil | | `te-IN` | Telugu | *** ## Supported audio formats `.wav` · `.mp3` · `.mp4` · `.flac` · `.ogg` · `.opus` · `.m4a` · `.aac` · `.webm` · `.amr` *** ## Job statuses | Status | Terminal | Meaning | | ----------------- | -------- | ---------------------------------- | | `CREATED` | No | Waiting for `/start` | | `STARTING` | No | Setup running | | `QUEUED` | No | Files queued | | `IN_PROGRESS` | No | Transcription running | | `COMPLETED` | Yes | All succeeded | | `PARTIAL_FAILURE` | Yes | Mixed results | | `FAILED` | Yes | Failed | | `START_FAILED` | Yes | Setup failed — see `cancel_reason` | | `CANCELLING` | No | Cancel in progress | | `CANCELLED` | Yes | Cancelled | *** ## Webhooks Pass `callback_url` at create time to receive a `POST` when the job reaches a terminal state. | `event` | Triggered when | | --------------------- | ----------------------- | | `job.completed` | All files succeeded | | `job.partial_failure` | Mixed success / failure | | `job.failed` | All files failed | | `job.cancelled` | Job cancelled | Treat webhooks as best-effort. Polling remains the source of truth. *** ## Common mistakes | Mistake | Fix | | ---------------------------------------------- | ---------------------------------------- | | Forgetting `/start` | Call **Start Job** after create | | Expecting `full_transcript` on status/`/files` | `GET /files` → then `GET transcript_url` | | Using `/v1/jobs/...` from the create message | Use `/stt/v3/batch/jobs/{job_id}/start` | | Polling every 1–2 seconds | Poll every 10s (30s for large jobs) | | Expired `transcript_url` | Re-call **Get Job Files** | *** ## Support Docs feedback: [Batch STT docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571) **Last verified:** 5 August 2026 # Speech-to-Text (Realtime) Source: https://docs.gnani.ai/api/STT/stt-websocket Real-time speech-to-text over a persistent WebSocket connection. ## Overview Stream raw PCM audio frames and receive transcript segments as speech is detected. The server uses Voice Activity Detection (VAD) to identify speech boundaries and returns a transcript for each segment. | Use case | Recommended endpoint | | ---------------------------------------------- | --------------------------------------- | | Live microphone / phone call / real-time audio | **This endpoint** | | Short pre-recorded clips ≤ 60 s | [STT REST](/api/STT/speech-to-text) | | Large files or bulk jobs | [STT Batch](/api/STTBatch/Introduction) | *** ## Endpoint ```text theme={null} WSS wss://api.vachana.ai/stt/v3/stream ``` *** ## Connection Headers All configuration is passed as WebSocket upgrade headers at connection time. Headers cannot be changed mid-session — reconnect with new headers to change settings. | Header | Required | Default | Description | | --------------------- | -------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `x-api-key-id` | Yes | — | Your Gnani API key. | | `lang_code` | Yes | `en-IN` | BCP-47 language code for transcription. See [Supported Languages](#supported-languages). | | `x-sample-rate` | No | `16000` | Sample rate of the audio stream in Hz. Accepted values: `8000`, `16000`, `44100`, `48000`. Must match the actual sample rate of your audio source. | | `x-format` | No | `verbatim` | `verbatim` — raw spoken-form output. `transcribe` — enables Inverse Text Normalization (ITN). See [ITN](#inverse-text-normalization-itn). | | `itn_native_numerals` | No | `false` | When `x-format=transcribe`, set `true` to render digits in the native script of the target language (e.g. `₹५,०००` instead of `₹5,000` for Hindi). | **Choosing the right sample rate:** | Value | When to use | | ------- | ------------------------------------------------- | | `48000` | Browser `getUserMedia` default; Mac microphone | | `44100` | Mac microphone alternate; CD-quality audio | | `16000` | Wideband telephony; sent as-is with no resampling | | `8000` | Narrowband telephony (legacy PSTN / VoIP) | *** ## Connection Flow A WebSocket session follows a strict sequence: 1. **Client connects** — opens a WebSocket to `/stt/v3/stream` with all required headers. 2. **Server confirms** — immediately sends a `connected` message echoing the active configuration. 3. **Client streams audio** — continuously sends binary frames of raw PCM audio at a steady real-time cadence. 4. **Server detects speech** — VAD identifies end-of-speech boundaries and emits a `processing` message to acknowledge that a segment was captured. 5. **Server returns transcript** — sends a `transcript` message with the transcribed text, segment metadata, and latency. 6. **Either side closes** — client or server may close the connection at any time. The `processing` message is a low-latency signal that audio was captured and transcription has begun. Expect a `transcript` message shortly after. *** ## Audio Format & Sending Audio All audio must be sent as **raw PCM binary frames** over the WebSocket. No container format (WAV, MP3, etc.) is accepted mid-stream. ### PCM Specification | Property | 16 kHz | 8 kHz | | ------------------- | --------------------------------------- | --------------------------------------- | | Encoding | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian | | Sample Rate | 16,000 Hz | 8,000 Hz | | Channels | 1 (mono) | 1 (mono) | | Samples per chunk | 512 | 512 | | **Bytes per frame** | **1,024 bytes** (512 samples × 2 bytes) | **1,024 bytes** (512 samples × 2 bytes) | | Frame duration | 32 ms | 64 ms | ### Sending Rules * Each binary frame must be **exactly 1,024 bytes**. * Frames must be sent at **real-time cadence** — one frame every 32 ms (16 kHz) or 64 ms (8 kHz). Do not buffer and burst; this degrades VAD accuracy. * For `44100` and `48000` Hz sources, the server resamples internally — still send 1,024-byte frames at the appropriate cadence. *** ## Server Messages The server sends JSON text frames. All messages share a `type` discriminator field and an ISO-8601 `timestamp`. ### `connected` Sent once immediately after the WebSocket handshake succeeds. ```json theme={null} { "type": "connected", "message": "STT service ready — VAD service connected", "timestamp": "2024-01-15T10:30:00.000Z", "config": { "sample_rate": 16000, "chunk_size": 512 } } ``` | Field | Type | Description | | -------------------- | --------- | ------------------------------------------------------ | | `type` | `string` | Always `"connected"`. | | `message` | `string` | Human-readable status string. | | `timestamp` | `string` | ISO-8601 server timestamp. | | `config.sample_rate` | `integer` | Active sample rate in Hz, echoed from `x-sample-rate`. | | `config.chunk_size` | `integer` | Expected chunk size in samples (always 512). | ### `processing` Emitted when VAD detects the end of a speech segment and transcription has begun. Use this as a low-latency acknowledgment that audio was captured. ```json theme={null} { "type": "processing", "timestamp": "2024-01-15T10:30:05.123Z" } ``` | Field | Type | Description | | ----------- | -------- | ------------------------------------------------ | | `type` | `string` | Always `"processing"`. | | `timestamp` | `string` | ISO-8601 timestamp when speech-end was detected. | ### `transcript` Contains the transcribed text for a completed speech segment. ```json theme={null} { "type": "transcript", "timestamp": "2024-01-15T10:30:05.987Z", "text": "Hello, how are you today?", "audio_duration_ms": 2340, "segment_id": "", "segment_index": 0, "latency": 320 } ``` | Field | Type | Description | | ------------------- | --------- | ---------------------------------------------------------------------------------------- | | `type` | `string` | Always `"transcript"`. | | `timestamp` | `string` | ISO-8601 timestamp when the transcript was emitted. | | `text` | `string` | Transcribed text. Format depends on the `x-format` header. | | `audio_duration_ms` | `integer` | Duration of the captured speech segment in milliseconds. | | `segment_id` | `string` | Unique identifier for this speech segment. Use for deduplication or support correlation. | | `segment_index` | `integer` | Sequential index of this segment within the session, starting at `0`. | | `latency` | `integer` | Time in milliseconds from end-of-speech detection to transcript delivery. | ### `error` Sent when the server encounters a recoverable or fatal error. The connection may remain open after a recoverable error. | Field | Type | Description | | ----------- | -------- | ---------------------------------------- | | `type` | `string` | Always `"error"`. | | `timestamp` | `string` | ISO-8601 timestamp of the error. | | `message` | `string` | Human-readable description of the error. | ```json theme={null} { "type": "error", "timestamp": "2024-01-15T10:30:10.000Z", "message": "STT engine failed to initialize" } ``` *** ## Python SDK The official Python SDK wraps the WebSocket connection, audio pacing, and event parsing into a clean async interface. ### Installation ```bash theme={null} pip install gnani-vachana ``` Requires **Python 3.10+**. ### Authentication The streaming client requires your API key and language code. ```python Constructor argument theme={null} from gnani.stt import GnaniSTTStreamClient stream = GnaniSTTStreamClient( api_key="your-api-key", language_code="hi-IN", ) ``` ```bash Environment variable theme={null} export GNANI_API_KEY="your-api-key" ``` ```python Environment variable (usage) theme={null} from gnani.stt import GnaniSTTStreamClient # Picks up GNANI_API_KEY from environment automatically stream = GnaniSTTStreamClient(language_code="hi-IN") ``` ### Stream Audio from a File Use the async context manager and the `stream_audio` helper. It handles real-time pacing automatically so frames are sent at the correct cadence for VAD. ```python theme={null} import asyncio from gnani.stt import GnaniSTTStreamClient async def main(): async with GnaniSTTStreamClient( api_key="your-api-key", language_code="hi-IN", sample_rate=16000, ) as stream: with open("audio.pcm", "rb") as f: transcripts = await stream.stream_audio( f, on_transcript=lambda t: print(f"Transcript: {t.text}"), on_processing=lambda p: print("Processing..."), realtime_pace=True, # sends frames at real-time cadence ) print(f"Total segments: {len(transcripts)}") asyncio.run(main()) ``` ### Iterate Over Events Manually For lower-level control — handling each event type differently or interleaving sending and receiving — iterate over the stream directly. ```python theme={null} import asyncio from gnani.stt import GnaniSTTStreamClient, StreamTranscriptEvent, StreamProcessingEvent async def main(): async with GnaniSTTStreamClient( api_key="your-api-key", language_code="hi-IN", ) as stream: with open("audio.pcm", "rb") as f: while chunk := f.read(1024): await stream.send_audio(chunk) await asyncio.sleep(0.032) # 32 ms per frame at 16 kHz async for event in stream: if isinstance(event, StreamTranscriptEvent): print(f"[Segment {event.segment_index}] {event.text}") print(f" Duration: {event.audio_duration_ms} ms Latency: {event.latency} ms") elif isinstance(event, StreamProcessingEvent): print("Processing speech...") asyncio.run(main()) ``` ### Using 8 kHz Audio (Telephony) ```python theme={null} stream = GnaniSTTStreamClient( api_key="your-api-key", language_code="en-IN", sample_rate=8000, ) ``` ### SDK Event Types All events are typed dataclasses with a `raw` field containing the full server JSON. | Event class | Key fields | Description | | ----------------------- | ------------------------------------------------------- | -------------------------------------------------------------------- | | `StreamConnectedEvent` | `message`, `sample_rate`, `chunk_size` | Sent once after the WebSocket handshake. Confirms the active config. | | `StreamProcessingEvent` | `timestamp` | VAD detected end-of-speech; transcription has started. | | `StreamTranscriptEvent` | `text`, `segment_index`, `audio_duration_ms`, `latency` | Completed transcript for a speech segment. | | `StreamErrorEvent` | `message`, `timestamp` | Server-side error, recoverable or fatal. | ### Error Handling ```python theme={null} from gnani.stt import ( StreamConnectionError, # Could not establish the WebSocket connection StreamClosedError, # Attempted to send on an already-closed stream StreamError, # Server returned an error message mid-session ) try: async with GnaniSTTStreamClient(api_key="your-api-key") as stream: await stream.send_audio(chunk) except StreamConnectionError as e: print(f"Could not connect: {e}") except StreamClosedError as e: print(f"Stream was already closed: {e}") except StreamError as e: print(f"Server error: {e.message} (at {e.timestamp})") ``` *** ## Supported Languages | Language | Code | Native Script | Example | | --------- | ------- | ---------------------------------- | ------------------------------- | | Bengali | `bn-IN` | Bengali (বাংলা) | "আমি ভাত খাই" | | English | `en-IN` | English (Latin) | "I am going to the market" | | Gujarati | `gu-IN` | Gujarati (ગુજરાતી) | "હું બજાર જાઉં છું" | | Hindi | `hi-IN` | Devanagari (हिन्दी) | "मैं बाज़ार जा रहा हूँ" | | Kannada | `kn-IN` | Kannada (ಕನ್ನಡ) | "ನಾನು ಮಾರುಕಟ್ಟೆಗೆ ಹೋಗುತ್ತೇನೆ" | | Malayalam | `ml-IN` | Malayalam (മലയാളം) | "ഞാൻ ചന്തയിലേക്ക് പോകുന്നു" | | Marathi | `mr-IN` | Devanagari (मराठी) | "मी बाजारात जातोय" | | Punjabi | `pa-IN` | Gurmukhi (ਪੰਜਾਬੀ) | "ਮੈਂ ਬਾਜ਼ਾਰ ਜਾ ਰਿਹਾ ਹਾਂ" | | Tamil | `ta-IN` | Tamil (தமிழ்) | "நான் சந்தைக்கு செல்கிறேன்" | | Telugu | `te-IN` | Telugu (తెలుగు) | "నేను మార్కెట్‌కి వెళ్తున్నాను" | *** ## Inverse Text Normalization (ITN) When `x-format: transcribe` is set, ITN runs on every transcript segment immediately after recognition — converting spoken-form numbers, currency, dates, times, and phone numbers into the compact written form a reader expects. ### What ITN Normalizes #### 1 — Cardinal & Ordinal Numbers | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------- | -------------------- | ----------------------- | | दो हज़ार | 2,000 | Indian comma grouping | | पाँच लाख बीस हज़ार | 5,20,000 | Lakh-scale grouping | | five lakh | 5,00,000 | English lakh convention | | पहला / twenty first | 1st / 21st | Ordinal suffix | #### 2 — Currency & Money | Spoken input (ASR) | Written output (ITN) | Rule | | --------------------------- | -------------------- | ---------------------- | | पाँच सौ रुपये | ₹500 | ₹ + amount | | तीन रुपये पचास पैसे | ₹3.50 | ₹ + rupees.paise | | I need five thousand rupees | ₹5,000 | English India pipeline | | pay do lakh rupees | ₹2,00,000 | Code-mixed en/hi | #### 3 — Dates | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------------------------ | -------------------- | ----------------------- | | बीस जनवरी दो हज़ार पच्चीस | 20 जनवरी 2025 | DD Month YYYY (hi) | | fifteenth january twenty twenty five | 15th January 2025 | Ordinal Month YYYY (en) | #### 4 — Times Indian time-of-day words (सुबह, दोपहर, शाम, रात) automatically map to 24-hour HH:MM output. | Spoken input (ASR) | Written output (ITN) | Rule | | -------------------------------------- | ---------------------------- | ----------------------- | | सुबह पाँच बजे | सुबह 05:00 | सुबह = AM | | शाम पाँच बजे | शाम 17:00 | शाम = evening (16–20 h) | | रात के दस बजे | रात 22:00 | रात = night (20–24 h) | | meeting at five fifteen in the evening | meeting 17:15 in the evening | en — 24-hour | #### 5 — Phone Numbers & PIN Codes | Spoken input (ASR) | Written output (ITN) | Rule | | ------------------------------------- | -------------------- | ------------------- | | नौ आठ सात छह पाँच चार तीन दो एक शून्य | 9876543210 | 10 digits → phone | | एक एक शून्य शून्य शून्य एक | 110001 | 6 digits → PIN | | one two three four five six | 123456 | English digit words | #### 6 — Mixed & Code-Mixed Utterances | Spoken input (ASR) | Written output (ITN) | | ----------------------------------------------------- | --------------------------------- | | कल थ्री फिफ्टी पीएम को पाँच सौ रुपये transfer करना है | कल 15:50 को ₹500 transfer करना है | | pay do lakh rupees by fifteenth march | pay ₹2,00,000 by 15th March | ### Native Script Digits — `itn_native_numerals` By default, ITN outputs Western Arabic digits (0–9). Set `itn_native_numerals: true` in the connection headers to render digits in the native script of the target language. | Language | Spoken input | `false` (default) | `true` — native script | | --------------- | -------------------- | ----------------- | -------------------------- | | Hindi `hi-IN` | पाँच हज़ार रुपये | ₹5,000 | ₹५,००० | | English `en-IN` | five thousand rupees | ₹5,000 | ₹5,000 (Latin — no change) | ### What ITN Does Not Change ITN intentionally preserves idiomatic and ambiguous phrases to avoid incorrect normalization. * **दो तीन** (meaning *a few*) stays as text, not `2` or `3` * **कर दो / ले दो** (imperative verbs) are kept as words, not treated as cardinal 2 *** # Cancel Job Source: https://docs.gnani.ai/api/STTBatch/Cancel_Job STTBatch.yml POST /stt/v3/batch/jobs/{job_id}/cancel Cancel a batch transcription job. ## Overview Cancel a batch job that is not already in a conflicting terminal state. | Current status | Result | | ------------------------------------- | ------------------------------- | | `CREATED` | Immediately `CANCELLED` (`200`) | | `STARTING` / `QUEUED` / `IN_PROGRESS` | `CANCELLING` → `CANCELLED` | | Already `CANCELLED` | `409 JOB_CANCELLED` | | Other terminal states | Conflict error | ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters In **Try it**, paste a real `job_id` from [Create Job](/api/STTBatch/Create_Job) before clicking Send. Leaving the placeholder `{job_id}` in the URL returns `422`. Job ID returned by [Create Job](/api/STTBatch/Create_Job). Replace with your own `job_id` before sending. ## Request Body Optional cancel reason. ## Example cURL ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs/019fa79c-7880-7d05-a528-edc7892ad9ea/cancel \ --header 'X-API-Key-ID: ' \ --header 'Content-Type: application/json' \ --data '{"reason":"Docs verification cancel test"}' ``` ## Response ### Success (`200`) ```json theme={null} { "job_id": "019fa79c-7880-7d05-a528-edc7892ad9ea", "status": "CANCELLED", "message": "Job cancelled successfully.", "cancelled_at": "2026-07-28T07:24:38.366982Z" } ``` ### Error responses **409 Already cancelled** ```json theme={null} { "error": "JOB_CANCELLED", "message": "Job is already cancelled" } ``` **404 Job not found** ```json theme={null} { "error": "JOB_NOT_FOUND", "message": "Job 01900000-0000-7000-8000-000000000000 not found" } ``` `CANCELLED` on success. Timestamp when the job was cancelled. ## Use Cases * Cancelling a job before start when files were uploaded by mistake * Stopping an in-progress batch when the pipeline is aborted # Create Job Source: https://docs.gnani.ai/api/STTBatch/Create_Job STTBatch.yml POST /stt/v3/batch/jobs Create a new batch transcription job with uploaded files, a ZIP, or public URLs. ## Overview Create a new batch transcription job. The job is created in `CREATED` status with your audio accepted — it does **not** start transcription yet. Call [Start Job](/api/STTBatch/Start_Job) next. | Method | Best for | Content-Type | | ------------------- | ---------------------------------------- | --------------------- | | Direct upload / ZIP | Files on your machine or CI | `multipart/form-data` | | Public URL | HTTPS links, including S3 presigned URLs | `application/json` | Authenticated private bucket access is **not** supported. The **Try it** playground uses the **public URL** (`application/json`) body below. For file upload, use the multipart cURL examples. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Request Body ### Public URL (`application/json`) — Try it | Field | Type | Required | Description | | ------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `config.model` | string | Yes | `"gnani-prisma-v2.5"` | | `config.language_code` | string | Yes | One code, e.g. `en-IN`. Or up to **3** comma-separated codes (`"hi-IN,en-IN"`) to have the language identified per file — see [Language identification](/api/STTBatch/Introduction#language-identification) | | `config.mode` | string | No | `"transcribe"` | | `config.with_diarization` | boolean | No | Default `false` | | `config.num_speakers` | integer | No | Required when diarization is on (max `2`) | | `config.is_multi_channel` | boolean | No | Default `false` | | `config.with_denoise` | boolean | No | Default `false`. Enhance (denoise) the audio before transcription — see [Speech enhancement](/api/STTBatch/Introduction#speech-enhancement-denoising) | | `source.type` | string | Yes | `"cloud_storage"` | | `source.auth.mode` | string | Yes | `"public"` | | `source.paths` | string\[] | Yes | Public HTTPS URLs (presigned URLs work). A path ending in `/` expands to every audio file under that prefix. **No 10 MB cap here** — cloud files are bounded by the 4-hour duration limit, not by size. See [Limits](/api/STTBatch/Introduction#limits) | | `callback_url` | string | No | Webhook URL — leave empty in Try it if unused | URLs are validated at **Start Job**, not at create. ### Multipart upload (`multipart/form-data`) Use cURL / your client — not the Try it panel. | Field | Type | Required | Description | | -------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `config` | string (JSON) | Yes | STT config. Required: `model`, `language_code` (one code, or up to 3 comma-separated — see [Language identification](/api/STTBatch/Introduction#language-identification)). Optional: `mode` (default `transcribe`), `with_diarization`, `num_speakers` (required when diarization is on, max `2`), `is_multi_channel`, `with_denoise` (default `false` — see [Speech enhancement](/api/STTBatch/Introduction#speech-enhancement-denoising)) | | `files` | file | Yes | Audio or ZIP. Max **100** files/job, **10 MB** per file, ZIP up to **50 MB** compressed / **200 MB** decompressed. These caps apply to uploads only — see [Limits](/api/STTBatch/Introduction#limits) | | `callback_url` | string | No | HTTPS webhook. See [Webhook sample payloads](/api/STTBatch/Introduction#webhooks) | Send `config` as a form field with `Content-Type: application/json` (see cURL `-F '...;type=application/json'`). ## Example cURL ### Direct upload (live-verified) ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs \ --header 'X-API-Key-ID: ' \ -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":false,"is_multi_channel":false};type=application/json' \ -F "files=@./customer_call.wav" ``` ### Public URL ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs \ --header 'Content-Type: application/json' \ --header 'X-API-Key-ID: ' \ --data '{ "config": { "model": "gnani-prisma-v2.5", "language_code": "en-IN", "mode": "transcribe" }, "source": { "type": "cloud_storage", "auth": { "mode": "public" }, "paths": ["https://cdn.example.com/audio/call-001.mp3"] } }' ``` ### Diarization (2 speakers) ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs \ --header 'X-API-Key-ID: ' \ -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":true,"num_speakers":2};type=application/json' \ -F "files=@./stereo_call.wav" ``` ### Speech enhancement (noisy audio) Set `with_denoise: true` to clean the recording before it is transcribed. Use it for noisy call-centre or field recordings; leave it off for clean studio audio, where it adds processing time without changing the transcript. See [Speech enhancement](/api/STTBatch/Introduction#speech-enhancement-denoising). ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs \ --header 'X-API-Key-ID: ' \ -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_denoise":true};type=application/json' \ -F "files=@./noisy_call.wav" ``` It works the same way on the public-URL body: ```json theme={null} { "config": { "model": "gnani-prisma-v2.5", "language_code": "en-IN", "mode": "transcribe", "with_denoise": true }, "source": { "type": "cloud_storage", "auth": { "mode": "public" }, "paths": ["https://cdn.example.com/audio/noisy-call-001.mp3"] } } ``` ## Response ### Success — multipart upload (`201`) ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "CREATED", "total_files_accepted": 1, "created_at": "2026-07-28T07:26:50.926489Z", "message": "1 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing." } ``` Save `job_id`. Always start with: ```text theme={null} POST /stt/v3/batch/jobs/{job_id}/start ``` (even if the message mentions `/v1/jobs/...`) ### Success — public URL create (`201`) ```json theme={null} { "job_id": "019fa79c-ec88-770c-b498-371fc21fb40e", "status": "CREATED", "created_at": "2026-07-28T07:25:07.137907Z", "message": "Job created. Call POST /stt/v3/batch/jobs/{job_id}/start to begin processing.", "total_files_accepted": null } ``` ### Error responses **401 Missing API key** ```json theme={null} { "detail": { "error_code": "MISSING_API_KEY", "message": "Missing API key", "status_code": 401 } } ``` **400 Unsupported language** ```json theme={null} { "error": "UNSUPPORTED_LANGUAGE", "message": "Value error, This service does not support language_code 'xx-XX'. Supported languages: bn-IN, en-IN, hi-IN, kn-IN, ml-IN, mr-IN, ta-IN, te-IN" } ``` **400 Too many languages** — more than three comma-separated codes ```json theme={null} { "error": "UNSUPPORTED_LANGUAGE", "message": "Value error, language_code accepts at most 3 comma-separated languages for identification, got 4: hi-IN, en-IN, kn-IN, ta-IN." } ``` Unique job identifier. Required for Start / Status / Files / Cancel. Always `CREATED` on success. Number of files accepted for multipart/ZIP uploads. May be `null` for public-URL creates. ## Webhook samples If you pass `callback_url`, Gnani sends a `POST` to your URL when the job finishes. Full payload examples (including `transcripts[]` / `full_transcript`): → **[Webhook samples on Batch STT Introduction](/api/STTBatch/Introduction#webhooks)** Events: `job.completed` · `job.partial_failure` · `job.failed` · `job.cancelled` ## Use Cases * Uploading one or many call recordings for offline transcription * Sending a ZIP of customer audio for batch processing * Pointing to public HTTPS / S3 presigned URLs without re-uploading files Next: [Start Job](/api/STTBatch/Start_Job) # Get Job Status Source: https://docs.gnani.ai/api/STTBatch/Get_Job STTBatch.yml GET /stt/v3/batch/jobs/{job_id} Get batch job status and progress counters. ## Overview Get the current status and progress of a batch job. This response does **not** include transcript text. After a terminal status, call [Get Job Files](/api/STTBatch/Get_Job_Files), then `GET` each `transcript_url`. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters In **Try it**, paste a real `job_id` from [Create Job](/api/STTBatch/Create_Job) before clicking Send. Leaving the placeholder `{job_id}` in the URL returns `422`. Job ID returned by [Create Job](/api/STTBatch/Create_Job). Replace with your own `job_id` before sending. ## Polling guidance Live lifecycle observed: ```text theme={null} CREATED → STARTING → QUEUED → IN_PROGRESS → COMPLETED ``` | Status | Terminal | Next step | | ----------------- | -------- | --------------------------------------------- | | `COMPLETED` | Yes | Call `/files`, then GET each `transcript_url` | | `PARTIAL_FAILURE` | Yes | Same — fetch successful files via `/files` | | `FAILED` | Yes | Inspect `/files` + `error_message` | | `START_FAILED` | Yes | Read `cancel_reason` | | `CANCELLED` | Yes | Done | Poll no faster than every **10 seconds**. For 100+ file jobs, prefer every **30 seconds**. ## Example cURL ```bash theme={null} curl --request GET \ --url https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30 \ --header 'X-API-Key-ID: ' ``` ## Response ### Success — completed (`200`) ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "COMPLETED", "config": { "model": "gnani-prisma-v2.5", "mode": "transcribe", "language_code": "en-IN", "with_diarization": false, "num_speakers": null, "is_multi_channel": false, "with_denoise": false }, "progress": { "total_files": 1, "completed_files": 1, "failed_files": 0, "in_progress_files": 0, "queued_files": 0, "cancelled_files": 0 }, "cancel_reason": null, "created_at": "2026-07-28T07:26:50.926489Z", "started_at": "2026-07-28T07:26:52.421366Z", "completed_at": "2026-07-28T07:27:24.621462Z", "cancelled_at": null, "updated_at": "2026-07-28T07:27:24.621676Z" } ``` ### Success — start failed (`200`) Returned when public URLs cannot be fetched: ```json theme={null} { "job_id": "019fa79c-ec88-770c-b498-371fc21fb40e", "status": "START_FAILED", "config": { "model": "gnani-prisma-v2.5", "mode": "transcribe", "language_code": "en-IN", "with_diarization": false, "num_speakers": null, "is_multi_channel": false, "with_denoise": false }, "progress": { "total_files": 0, "completed_files": 0, "failed_files": 0, "in_progress_files": 0, "queued_files": 0, "cancelled_files": 0 }, "cancel_reason": "All provided paths were invalid — nothing to process.", "created_at": "2026-07-28T07:25:07.137907Z", "started_at": null, "completed_at": null, "cancelled_at": null, "updated_at": "2026-07-28T07:27:28.390415Z" } ``` ### Error responses **404 Job not found** ```json theme={null} { "error": "JOB_NOT_FOUND", "message": "Job 01900000-0000-7000-8000-000000000000 not found" } ``` Current job status (`CREATED`, `QUEUED`, `IN_PROGRESS`, `COMPLETED`, `START_FAILED`, …). Per-file counters: total, completed, failed, in progress, queued, cancelled. Present for failures such as `START_FAILED`. The config the job was created with, echoed back — including `language_code`, `with_diarization` and `with_denoise`. ## Use Cases * Polling until a job reaches a terminal status * Debugging `START_FAILED` via `cancel_reason` Next: [Get Job Files](/api/STTBatch/Get_Job_Files) # Get Job Files Source: https://docs.gnani.ai/api/STTBatch/Get_Job_Files STTBatch.yml GET /stt/v3/batch/jobs/{job_id}/files List per-file results and transcript URLs for a batch job. ## Overview List per-file results for a job. For completed files, this returns `transcript_url` — **not** the transcript text. To get `full_transcript` without webhooks: 1. Call this endpoint after the job is terminal (filter with `status=COMPLETED`) 2. `GET` each `transcript_url` (no API key — pre-signed URL) 3. Read `full_transcript` from the JSON `transcript_url` expires in **1 hour**. Re-call this endpoint for a fresh URL. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters In **Try it**, paste a real `job_id` from [Create Job](/api/STTBatch/Create_Job) before clicking Send. Leaving the placeholder `{job_id}` in the URL returns `422`. Job ID returned by [Create Job](/api/STTBatch/Create_Job). Replace with your own `job_id` before sending. ## Query Parameters Filter by file status. Use `COMPLETED` for successful transcripts. Omit to include `FAILED` / `SKIPPED`. Page size (1–100). Pagination cursor from `pagination.next_cursor`. ## Example cURL ```bash theme={null} curl --request GET \ --url 'https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30/files?status=COMPLETED' \ --header 'X-API-Key-ID: ' ``` Download transcript text: ```bash theme={null} curl -L "" ``` ## Response ### Success — completed file (`200`) ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "data": [ { "file_id": "6f5e3bcd-add2-479c-8883-a9c27b221f10", "original_path": "customer_call.wav", "status": "COMPLETED", "duration_seconds": "6.01", "transcript_url": "https://gnaniasraudios.s3.amazonaws.com/results/019fa79e-81f9-7a8a-a446-6eff16ddec30/6f5e3bcd-add2-479c-8883-a9c27b221f10.json?X-Amz-Expires=3600&...", "error_message": null, "created_at": "2026-07-28T07:26:50.926489Z", "completed_at": "2026-07-28T07:27:24.513422Z" } ], "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } ``` ### transcript\_url JSON (live-verified) `language_code` here is the language the file was **actually transcribed in**. When you submit a comma-separated list, this is the single language that was identified — not the list you sent — so it always tells you what the text is in. Per-segment `language_detected` is currently always `null`. ```json theme={null} { "file_id": "6f5e3bcd-add2-479c-8883-a9c27b221f10", "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "original_path": "customer_call.wav", "language_code": "en-IN", "duration_seconds": 6.01, "model": "gnani-prisma-v2.5", "mode": "transcribe", "segments": [ { "segment_id": 0, "start_time": 0.0, "end_time": 0.71, "text": "hello", "speaker_id": 1, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null }, { "segment_id": 0, "start_time": 0.71, "end_time": 2.36, "text": "how can i help you today", "speaker_id": 2, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null }, { "segment_id": 0, "start_time": 2.36, "end_time": 6.01, "text": "i am calling about my recent order number 12345", "speaker_id": 2, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null } ], "full_transcript": "hello how can i help you today i am calling about my recent order number 12345", "created_at": "2026-07-28T07:27:24Z" } ``` ### Failed file — empty / non-speech audio ```json theme={null} { "job_id": "019fa79b-a07a-7a3e-93bf-4ee73974dfa2", "data": [ { "file_id": "e0323a1c-86ee-4898-aee7-7455dfc1eb52", "original_path": "customer_call.wav", "status": "FAILED", "duration_seconds": null, "transcript_url": null, "error_message": "Empty transcript after 3 retries", "created_at": "2026-07-28T07:23:42.170454Z", "completed_at": "2026-07-28T07:24:32.106034Z" } ], "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } ``` ### Skipped file — invalid public URL ```json theme={null} { "job_id": "019fa79c-ec88-770c-b498-371fc21fb40e", "data": [ { "file_id": "7539fecd-3090-4b62-9172-01c7019430d3", "original_path": "https://cdn.example.com/audio/missing.wav", "status": "SKIPPED", "duration_seconds": null, "transcript_url": null, "error_message": "REQUEST_ERROR: [Errno -2] Name or service not known", "created_at": "2026-07-28T07:27:27.650893Z", "completed_at": null } ], "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } ``` | File status | Meaning | | ----------- | ----------------------------------------- | | `COMPLETED` | Done — `transcript_url` available | | `FAILED` | Failed — see `error_message` | | `SKIPPED` | Not processed (bad URL / format / access) | | `CANCELLED` | Skipped due to job cancel | Pre-signed URL to transcript JSON. Expires in 1 hour. No API key required to download. Present when `status` is `FAILED` or `SKIPPED`. ## Use Cases * Fetching transcript download links after a job completes * Inspecting failed / skipped files via `error_message` # Batch STT Introduction Source: https://docs.gnani.ai/api/STTBatch/Introduction Asynchronous transcription of long or multiple audio files via the Batch Jobs API. Use **STT Batch** to transcribe long recordings or many files asynchronously. **Flow:** create a job → start it → poll until it finishes → download transcripts\ (or receive them on a webhook). | If you need… | Use | | -------------------------------------------------- | -------------------------------------- | | Long files, many files, or offline pipelines | **STT Batch** (this section) | | Short clips (under \~60 seconds), immediate answer | [STT REST](/api/STT/speech-to-text) | | Live microphone / streaming audio | [STT Realtime](/api/STT/stt-websocket) | *** ## How it works ```text theme={null} 1. CREATE POST /stt/v3/batch/jobs → 201 job_id status: CREATED 2. START POST /stt/v3/batch/jobs/{job_id}/start → 202 status: STARTING 3. WAIT GET /stt/v3/batch/jobs/{job_id} poll every 10s until a terminal status (live: STARTING → QUEUED → IN_PROGRESS → COMPLETED) 4. FILES GET /stt/v3/batch/jobs/{job_id}/files → transcript_url for each completed file 5. DOWNLOAD GET → JSON with full_transcript + segments ``` **Important:** creating a job does **not** start transcription. You must call `/start`. *** ## Interactive API reference Each endpoint has a **Try it** playground. Paste your `X-API-Key-ID` and run live requests from the docs. | Step | Endpoint | Page | | ---- | ----------------------------------------- | -------------------------------------------- | | 1 | `POST /stt/v3/batch/jobs` | [Create Job](/api/STTBatch/Create_Job) | | 2 | `POST /stt/v3/batch/jobs/{job_id}/start` | [Start Job](/api/STTBatch/Start_Job) | | 3 | `GET /stt/v3/batch/jobs/{job_id}` | [Get Job Status](/api/STTBatch/Get_Job) | | 4 | `GET /stt/v3/batch/jobs/{job_id}/files` | [Get Job Files](/api/STTBatch/Get_Job_Files) | | — | `GET /stt/v3/batch/jobs` | [List Jobs](/api/STTBatch/List_Jobs) | | — | `POST /stt/v3/batch/jobs/{job_id}/cancel` | [Cancel Job](/api/STTBatch/Cancel_Job) | **Webhook payload examples:** [Webhooks](#webhooks) — samples for `job.completed`, `job.failed`, and `job.cancelled`. *** ## Before you begin 1. An API key from the Gnani APIs dashboard 2. Audio with **real speech** in a [supported format](#supported-audio-formats) 3. A [supported language code](#supported-languages) **Base URL** ```text theme={null} https://api.vachana.ai ``` **Auth** — send on every Batch API request: ```http theme={null} X-API-Key-ID: ``` Tone-only / silent audio is accepted into a job but fails later with `Empty transcript after 3 retries`. *** ## How do I get the transcript text? | Option | When to use | What you do | | ------------------------ | ------------------------- | ------------------------------------------------------------------------------------- | | **A. Polling (default)** | No webhook | Poll job status → `GET /files` → `GET` each `transcript_url` → read `full_transcript` | | **B. Webhook** | You passed `callback_url` | Receive a POST with `transcripts[]` including `full_transcript` | `GET /jobs/{job_id}` and `GET /jobs/{job_id}/files` do **not** return `full_transcript`. *** ## Quickstart (live-verified) ### 1. Create ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs \ --header 'X-API-Key-ID: ' \ -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":false,"is_multi_channel":false};type=application/json' \ -F "files=@./customer_call.wav" ``` ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "CREATED", "total_files_accepted": 1, "created_at": "2026-07-28T07:26:50.926489Z", "message": "1 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing." } ``` ### 2. Start ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30/start \ --header 'X-API-Key-ID: ' ``` ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "STARTING", "message": "Job start accepted. File validation and queueing are running in the background. Poll GET /stt/v3/batch/jobs/{job_id} to track progress." } ``` ### 3. Poll until `COMPLETED` ```bash theme={null} curl --request GET \ --url https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30 \ --header 'X-API-Key-ID: ' ``` ### 4. Get `transcript_url`, then download text ```bash theme={null} curl --request GET \ --url 'https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30/files?status=COMPLETED' \ --header 'X-API-Key-ID: ' ``` ```bash theme={null} curl -L "" ``` ```json theme={null} { "full_transcript": "hello how can i help you today i am calling about my recent order number 12345", "model": "gnani-prisma-v2.5", "language_code": "en-IN", "duration_seconds": 6.01 } ``` *** ## Configuration | Field | Type | Required | Default | Description | | ------------------ | ------- | ------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | — | `"gnani-prisma-v2.5"` | | `language_code` | string | Yes | — | See [Supported languages](#supported-languages) | | `mode` | string | No | `"transcribe"` | Use `"transcribe"` for now | | `with_diarization` | boolean | No | `false` | Enable speaker diarization | | `num_speakers` | integer | Yes, if diarization is on | — | Max **2** | | `is_multi_channel` | boolean | No | `false` | `true` only for true multi-channel audio | | `with_denoise` | boolean | No | `false` | Enhance (denoise) the audio before transcription — see [Speech enhancement](#speech-enhancement-denoising) | Inverse Text Normalization (ITN) is **not** supported for STT Batch. ### Speech enhancement (denoising) Set `with_denoise: true` to have each file cleaned up before it is segmented and transcribed: ```json theme={null} "config": { "model": "gnani-prisma-v2.5", "language_code": "en-IN", "with_denoise": true } ``` It is **off by default**, and worth turning on only when your audio needs it: | Your audio | Recommendation | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Noisy calls, background hum, street or field recordings | Enable — this is what it is for | | Clean studio or headset audio | Leave off — enhancement passes clean speech through nearly unchanged, so you only pay the extra processing time | Notes: * The whole pipeline sees the cleaned audio — speech detection, transcription and diarization all run on it, so timestamps and speaker labels come from the enhanced signal. * It **adds processing work** per file — on the order of a quarter of the audio's duration again — so a job with `with_denoise` takes longer to reach `COMPLETED`. * It is **not conditional on how noisy the file is** — every file in the job is enhanced. Set it per job, on the jobs that need it. * Nothing else changes: the transcript JSON, webhook payloads and billing are the same (billing is per audio duration either way). * The setting is echoed back as `config.with_denoise` on [Get Job Status](/api/STTBatch/Get_Job). Enhancement is applied where the platform has it enabled. If it is switched off for maintenance, a job that asked for it still runs to `COMPLETED` — transcribed from the original audio rather than failing. *** ## Limits Limits depend on **how you send the audio**. The size and file-count limits below apply to **direct upload only** — they do not apply to files fetched from cloud storage. ### Every file, whichever way it arrives | Limit | Value | | ---------------------------- | -------------------- | | Max audio duration | **4 hours** per file | | Min audio duration | **0.1 s** per file | | Max speech segments detected | **5,000** per file | A file longer than 4 hours is rejected with `AUDIO_TOO_LONG`. Split it first. ### Direct upload (`multipart/form-data`) | Limit | Value | | ------------------------- | ---------- | | Max audio files per job | **100** | | Max size per file | **10 MB** | | Max ZIP compressed size | **50 MB** | | Max ZIP decompressed size | **200 MB** | | Max ZIP compression ratio | **50:1** | The 100-file limit counts everything in the job, including files expanded out of a ZIP. ### Cloud storage (`source.type: "cloud_storage"`) | Limit | Value | | -------------------------- | ------------------------------------------------------------ | | Max size per file | **No byte limit** — bounded by the 4-hour duration cap above | | Max download time per file | **30 minutes** | | Max files per job | **Not currently capped** | **The 10 MB limit is for uploads only.** Sending a 200 MB recording as a `cloud_storage` path is fine — what matters is that it is under 4 hours and downloadable within 30 minutes. Uploading that same file through `multipart/form-data` would be rejected. A path ending in `/` is treated as a **folder** and expands to every supported audio file under that prefix. Because cloud storage jobs are not file-count capped, a broad prefix can enqueue far more audio than you intended — and every file is billed. Point at explicit file paths, or a narrow prefix you have checked. ### Results and polling | Limit | Value | | ----------------------- | ------------------------------------------- | | `transcript_url` expiry | **1 hour** (`X-Amz-Expires=3600`) | | Poll interval | **≥ 10 s** (prefer **30 s** for 100+ files) | The poll interval is guidance, not an enforced limit — but the API is rate limited, so polling more tightly than this risks `429` responses on your other calls. *** ## Supported languages | Code | Language | | ------- | --------------- | | `bn-IN` | Bengali (India) | | `en-IN` | English (India) | | `hi-IN` | Hindi | | `kn-IN` | Kannada | | `ml-IN` | Malayalam | | `mr-IN` | Marathi | | `ta-IN` | Tamil | | `te-IN` | Telugu | **Batch vs REST/Realtime:** Gujarati (`gu-IN`) and Punjabi (`pa-IN`) are supported on [STT REST](/api/STT/speech-to-text) and [STT Realtime](/api/STT/stt-websocket) but **not** on Batch STT. Use REST or Realtime for those languages. ### Language identification If you know the language, pass a single code and it is used for the whole file. If you do not, pass **up to three** codes as a comma-separated list. Each file is then identified as **one** of exactly those languages and transcribed in it: ```json theme={null} "language_code": "hi-IN,en-IN" ``` Rules: * **Maximum three** codes. More returns `400 UNSUPPORTED_LANGUAGE`. * Every code must be from the table above; one unsupported code rejects the request. * **Order matters.** The **first** code is the fallback used when identification is not confident enough to choose. * The decision is made **per file**, not per segment — a file is transcribed entirely in one language. The resolved language is returned as `language_code` in the transcript JSON, so it always tells you what the text is actually in. * A single code behaves exactly as before, with no identification step. ## Supported audio formats `.wav` · `.mp3` · `.mp4` · `.flac` · `.ogg` · `.opus` · `.m4a` · `.aac` · `.webm` · `.amr` *** ## Job statuses | Status | Terminal | Meaning | | ----------------- | -------- | ---------------------------------- | | `CREATED` | No | Waiting for `/start` | | `STARTING` | No | Setup running | | `QUEUED` | No | Files queued | | `IN_PROGRESS` | No | Transcription running | | `COMPLETED` | Yes | All succeeded | | `PARTIAL_FAILURE` | Yes | Mixed results | | `FAILED` | Yes | Failed | | `START_FAILED` | Yes | Setup failed — see `cancel_reason` | | `CANCELLING` | No | Cancel in progress | | `CANCELLED` | Yes | Cancelled | *** ## Webhooks Pass `callback_url` at create time to receive a `POST` when the job reaches a terminal state. | `event` | Triggered when | | --------------------- | ----------------------- | | `job.completed` | All files succeeded | | `job.partial_failure` | Mixed success / failure | | `job.failed` | All files failed | | `job.cancelled` | Job cancelled | The `transcripts` array includes full per-file transcript JSON (with `full_transcript`), so webhook users usually do not need `/files` + `transcript_url`. Treat webhooks as best-effort. Polling remains the source of truth. ### Sample webhook payload — `job.completed` ```json theme={null} { "event": "job.completed", "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "org_id": "org_acme123", "user_id": "user_abc456", "status": "COMPLETED", "total_files": 1, "completed_files": 1, "failed_files": 0, "cancelled_files": 0, "completed_at": "2026-07-28T07:27:24Z", "callback_url": "https://yourapp.com/webhooks/stt", "transcripts": [ { "file_id": "6f5e3bcd-add2-479c-8883-a9c27b221f10", "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "original_path": "customer_call.wav", "language_code": "en-IN", "duration_seconds": 6.01, "model": "gnani-prisma-v2.5", "mode": "transcribe", "segments": [ { "segment_id": 0, "start_time": 0.0, "end_time": 0.71, "text": "hello", "speaker_id": 1, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null }, { "segment_id": 0, "start_time": 0.71, "end_time": 2.36, "text": "how can i help you today", "speaker_id": 2, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null }, { "segment_id": 0, "start_time": 2.36, "end_time": 6.01, "text": "i am calling about my recent order number 12345", "speaker_id": 2, "confidence": null, "language_detected": null, "sentiment": null, "emotion": null } ], "full_transcript": "hello how can i help you today i am calling about my recent order number 12345", "created_at": "2026-07-28T07:27:24Z" } ] } ``` ### Sample webhook payload — `job.failed` ```json theme={null} { "event": "job.failed", "job_id": "019fa79b-a07a-7a3e-93bf-4ee73974dfa2", "org_id": "org_acme123", "user_id": "user_abc456", "status": "FAILED", "total_files": 1, "completed_files": 0, "failed_files": 1, "cancelled_files": 0, "completed_at": "2026-07-28T07:24:32Z", "callback_url": "https://yourapp.com/webhooks/stt", "transcripts": [] } ``` ### Sample webhook payload — `job.cancelled` ```json theme={null} { "event": "job.cancelled", "job_id": "019fa79c-7880-7d05-a528-edc7892ad9ea", "org_id": "org_acme123", "user_id": "user_abc456", "status": "CANCELLED", "total_files": 1, "completed_files": 0, "failed_files": 0, "cancelled_files": 1, "completed_at": null, "callback_url": "https://yourapp.com/webhooks/stt", "transcripts": [] } ``` *** ## Common mistakes | Mistake | What happens | Fix | | ---------------------------------------------- | --------------------------------------------- | ---------------------------------------------------- | | Forgetting `/start` | Job stays `CREATED` | Call [Start Job](/api/STTBatch/Start_Job) | | Expecting `full_transcript` on status/`/files` | Field missing | `GET /files` → then `GET transcript_url` | | Using `/v1/jobs/...` from create message | Wrong path | Use `/stt/v3/batch/jobs/{job_id}/start` | | Uploading silence / tones | `FAILED` — `Empty transcript after 3 retries` | Use real speech | | Invalid public URL | `START_FAILED` / file `SKIPPED` | Use reachable HTTPS URLs | | Polling every 1–2 seconds | Wasted calls | Poll every 10s | | `with_denoise: true` on clean audio | Job takes longer, transcript barely changes | Enable it only for noisy recordings | | Expired `transcript_url` | Download fails | Re-call [Get Job Files](/api/STTBatch/Get_Job_Files) | *** ## Support Docs feedback: [Batch STT docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571) **Last verified:** 5 August 2026 Live-checked: Create → Start → Poll → Files → Transcript download, List, Cancel, and error codes `401` / `400` / `404` / `409`. # List Jobs Source: https://docs.gnani.ai/api/STTBatch/List_Jobs STTBatch.yml GET /stt/v3/batch/jobs List batch transcription jobs with optional status filter and pagination. ## Overview List batch jobs for your organization. Use filters and cursor pagination to browse history. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Query Parameters Filter by job status (`CREATED`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, `CANCELLED`, etc.). Page size (1–100). Pagination cursor from `pagination.next_cursor`. ## Example cURL ```bash theme={null} curl --request GET \ --url 'https://api.vachana.ai/stt/v3/batch/jobs?limit=3' \ --header 'X-API-Key-ID: ' ``` ## Response ### Success (`200`) ```json theme={null} { "data": [ { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "COMPLETED", "total_files": 1, "completed_files": 1, "failed_files": 0, "created_at": "2026-07-28T07:26:50.926489Z", "completed_at": "2026-07-28T07:27:24.621462Z" }, { "job_id": "019fa79b-a07a-7a3e-93bf-4ee73974dfa2", "status": "FAILED", "total_files": 1, "completed_files": 0, "failed_files": 1, "created_at": "2026-07-28T07:23:42.170454Z", "completed_at": "2026-07-28T07:24:32.176627Z" } ], "pagination": { "has_more": true, "next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDctMjdUMDc6NDM6NTguMDU1NzMzKzAwOjAwIiwgImlkIjogIjAxOWZhMjg3LWNmNjItNzE2OC1iM2MyLTQ1ZjExOGFjNzYxZCJ9" } } ``` List of jobs with status and file counters. Total files known for the job. May be `0` while still in `CREATED`. Whether another page exists. Pass as `cursor` on the next request. ## Use Cases * Reviewing recent batch jobs in your org * Filtering failed or in-progress jobs for ops dashboards # Start Job Source: https://docs.gnani.ai/api/STTBatch/Start_Job STTBatch.yml POST /stt/v3/batch/jobs/{job_id}/start Start processing a batch transcription job. ## Overview Start transcription for a job in `CREATED` status. Returns `202 Accepted` while validation and queueing run in the background. Creating a job does **not** start processing — you must call this endpoint. For public-URL jobs, path validation happens here. If every path fails, status becomes `START_FAILED`. ## Authentication | Header | Required | Description | | -------------- | -------- | ------------------------------ | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key | ## Path Parameters In **Try it**, paste a real `job_id` from [Create Job](/api/STTBatch/Create_Job) before clicking Send. Leaving the placeholder `{job_id}` in the URL returns `422`. Job ID returned by [Create Job](/api/STTBatch/Create_Job). Replace with your own `job_id` before sending. ## Example cURL ```bash theme={null} curl --request POST \ --url https://api.vachana.ai/stt/v3/batch/jobs/019fa79e-81f9-7a8a-a446-6eff16ddec30/start \ --header 'X-API-Key-ID: ' ``` ## Response ### Success (`202`) ```json theme={null} { "job_id": "019fa79e-81f9-7a8a-a446-6eff16ddec30", "status": "STARTING", "message": "Job start accepted. File validation and queueing are running in the background. Poll GET /stt/v3/batch/jobs/{job_id} to track progress." } ``` ### Error responses **409 Already started / terminal** ```json theme={null} { "error": "JOB_ALREADY_STARTED", "message": "Job cannot be restarted from state 'FAILED'" } ``` **404 Job not found** ```json theme={null} { "error": "JOB_NOT_FOUND", "message": "Job 01900000-0000-7000-8000-000000000000 not found" } ``` Job identifier. Typically `STARTING` after a successful start. ## Use Cases * Kicking off transcription after files are uploaded * Starting a public-URL job after create Next: poll [Get Job Status](/api/STTBatch/Get_Job) every **10 seconds** (prefer **30 seconds** for 100+ file jobs). # API Probe Source: https://docs.gnani.ai/api/STTBatch/_api_probe POST https://api.vachana.ai/stt/v3/batch/jobs Probe manual API playground ## Overview Probe page for manual api frontmatter. # Available Voices Source: https://docs.gnani.ai/api/TTS/available-voices Voice catalog for Gnani Timbre v2.5. ## Available Voices Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. 42 voices are available for Gnani Timbre v2.5. Each voice has a **preferred language** — set the `language` parameter to match for best results. Pass `auto` to detect language from the input script. *** ## Preferred Voices by Language Preview voices before you integrate. Open the [Gnani TTS playground](https://app.gnani.ai/voice/text-to-speech) and use the filters to narrow by **language**, **gender**, and **persona** — then listen to find the right match for your use case. | Language | `language` value | Voices | | :-------- | :--------------- | :---------------------------------------------------------------------------------------------------- | | English | `en-IN` | Kaveri, Trupti, Devika, Pranav, Shlok, Girish | | Hindi | `hi-IN` | Nalini, Bhavna, Yashvi, Urmila, Jwala, Chitra, Ambuja, Deepak, Roopesh, Vikrant, Hemraj, Jalaj, Omkar | | Hinglish | `hi-en` | Poorvi | | Tamil | `ta-IN` | Asmita, Trisha, Brinda, Vedika, Noopur | | Telugu | `te-IN` | Suhana, Lehara, Lavanya, Yukti, Varuni | | Kannada | `kn-IN` | Saanvi, Kavin | | Malayalam | `ml-IN` | Reshma, Riyaan | | Marathi | `mr-IN` | Zahira, Ishaan | | Bengali | `bn-IN` | Kirra, Dhruva | | Gujarati | `gu-IN` | Falak, Veera | | Punjabi | `pa-IN` | Mehuli, Zayan | Use each voice with its preferred `language` value. Voices listed under a language are optimized for that locale — using a mismatched `language` may reduce quality. *** ## Voice Library | Voice | Gender | Recommended language | Persona | Description | | :------ | :----- | :------------------- | :----------------------- | :------------------------------------------------ | | Ambuja | Female | Hindi | Professional | Clear, composed virtual assistant | | Asmita | Female | Tamil | Professional | Polished, refined, smooth corporate voice | | Bhavna | Female | Hindi | Friendly | Friendly voice for customer support | | Brinda | Female | Tamil | Professional | Steady, even-paced professional voice | | Chitra | Female | Hindi | Warm, empathetic | Mature voice for customer care | | Deepak | Male | Hindi | Professional | Clear, measured voice for corporate use | | Devika | Female | English | Professional | Authoritative voice for customer support | | Dhruva | Male | Bengali | Conversational | Casual, relatable voice for conversations | | Falak | Female | Gujarati | Conversational | Youthful, clear voice for narration | | Girish | Male | English | Expressive, energetic | Warm voice with natural storytelling | | Hemraj | Male | Hindi | Confident, authoritative | Confident voice for demos | | Ishaan | Male | Marathi | Professional | Clear voice for instructions and narration | | Jalaj | Male | Hindi | Expressive, energetic | Expressive voice for storytelling | | Jwala | Female | Hindi | Friendly | Friendly, approachable everyday voice | | Kaveri | Female | English | Professional | Firm voice for customer support | | Kavin | Male | Kannada | Professional | Firm, articulate voice for presentations | | Kirra | Female | Bengali | Conversational | Soft-spoken voice for casual conversations | | Lavanya | Female | Telugu | Warm, empathetic | Soft, empathetic voice for care and support | | Lehara | Female | Telugu | Warm, empathetic | Gentle, reassuring voice for comfort and trust | | Mehuli | Female | Punjabi | Expressive, energetic | Expressive voice for commercials and promotions | | Nalini | Female | Hindi | Friendly | Friendly voice for playful conversations | | Noopur | Female | Tamil | Friendly | Warm voice for everyday conversations | | Omkar | Male | Hindi | Confident, authoritative | Energetic voice for customer support | | Poorvi | Female | Hinglish | Conversational | Natural voice for bilingual conversations | | Pranav | Male | English | Friendly | Friendly voice for customer support | | Reshma | Female | Malayalam | Friendly | Bright voice for greetings and support | | Riyaan | Male | Malayalam | Conversational | Easygoing voice for casual conversations | | Roopesh | Male | Hindi | Professional | Grounded, knowledgeable, professional voice | | Saanvi | Female | Kannada | Expressive, energetic | Lively voice for ads and promotions | | Shlok | Male | English | Conversational | Warm, conversational voice | | Suhana | Female | Telugu | Warm, empathetic | Calm, soothing voice with confidence | | Trisha | Female | Tamil | Friendly | Warm, approachable voice for guidance | | Trupti | Female | English | Professional | Upbeat, articulate voice with maturity | | Urmila | Female | Hindi | Professional | Refined voice for formal announcements | | Varuni | Female | Telugu | Conversational | Clear, natural voice for everyday use | | Vedika | Female | Tamil | Professional | Crisp, articulate voice for information | | Veera | Male | Gujarati | Friendly | Friendly voice for casual conversations | | Vikrant | Male | Hindi | Conversational | Warm, genuine conversational voice | | Yashvi | Female | Hindi | Warm, empathetic | Gentle, reassuring voice for comfort | | Yukti | Female | Telugu | Friendly | Warm, welcoming voice that puts listeners at ease | | Zahira | Female | Marathi | Expressive, energetic | Energetic voice for sales and casual chat | | Zayan | Male | Punjabi | Warm, empathetic | Soft, caring voice for empathetic conversations | # Text-to-Speech (REST) Source: https://docs.gnani.ai/api/TTS/tts-inference POST /api/v1/tts/inference Synchronous text-to-speech with full audio returned in one response. ## Overview Get the complete synthesized audio in one response. Best for downloads or batch processing. For streaming playback, see [TTS Streaming](/api/TTS/tts-sse) or [TTS Realtime](/api/TTS/tts-websocket). Passing numbers, IDs, dates, or currency as raw strings causes mispronunciations. See the [Input Formatting Guide](/api/TTS/tts-input-formating) for correct formatting of phone numbers, account numbers, PINs, Aadhaar, vehicle registration numbers, GSTIN, currency, and more. *** ## Models | Model | `model` value | Languages | Extra parameters | | :---------- | :------------ | :------------------ | :------------------ | | Timbre v2.5 | `timbre-v2.5` | 10 Indian languages | `language`, `speed` | Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. *** ## Endpoint ```text theme={null} POST https://api.vachana.ai/api/v1/tts/inference ``` *** ## Authentication | Header | Required | Description | | :------------- | :------- | :----------------- | | `X-API-Key-ID` | Yes | Your Gnani API key | *** ## Request Parameters ```json theme={null} { "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" } } ``` The text to synthesize into speech. Pass numbers, dates, and currency as spoken words to avoid mispronunciations — see [Input Formatting Guide](/api/TTS/tts-input-formating). Set to `timbre-v2.5`. Voice name from the Timbre v2.5 catalog. See the [Voice Catalog](/api/TTS/available-voices) for preferred voices by language. Example: `Nalini`, `Kaveri`, `Deepak`. Language of the input text. Controls text normalization and routing. | Value | Language | | :------ | :------------------------------------------------------ | | `auto` | Auto-detect from script (recommended for mixed content) | | `hi-IN` | Hindi | | `en-IN` | English | | `ta-IN` | Tamil | | `te-IN` | Telugu | | `kn-IN` | Kannada | | `ml-IN` | Malayalam | | `mr-IN` | Marathi | | `pa-IN` | Punjabi | | `bn-IN` | Bengali | | `gu-IN` | Gujarati | | `hi-en` | Hinglish (code-mixed Hindi–English) | Playback speed multiplier. Numeric range: `0.85` (slowest) to `1.15` (fastest). String shortcuts: `"slow"` = 0.85, `"medium"` = 1.0, `"fast"` = 1.15. Controls the format of the returned audio. Sample rate in Hz. Supported: `8000`, `16000`, `22050`, `24000`, `44100`, `48000`. Number of audio channels. `1` for mono. Sample width in bytes. `2` for 16-bit audio. Audio encoding format. Options: `linear_pcm`, `pcm_s16le`, `pcm_mulaw`, `pcm_alaw`, `oggopus`. For telephony, prefer `container=mulaw` or `container=alaw` over this field — both produce the same output. Use `oggopus` (or `container=ogg`) for a playable OGG Opus file. Not required when `container` is `mp3`. Output container format. Options: `wav`, `raw`, `mp3`, `ogg`, `mulaw`, `alaw`. Use `ogg` for OGG Opus. Use `mulaw` or `alaw` for G.711 telephony (forces 8000 Hz). MP3 bitrate. Only used when `container` is `mp3`. Supported: `32k`, `64k`, `96k`, `128k`, `192k`. *** ## Audio Format Reference | `container` | `encoding` | Output | `sample_rate` | `bitrate` | Content-Type | | ----------- | ------------ | ---------------------- | ------------------ | ------------ | -------------------------- | | `wav` | `linear_pcm` | WAV file (with header) | 8000–48000 Hz | — | `audio/wav` | | `raw` | `linear_pcm` | Raw 16-bit PCM | 8000–48000 Hz | — | `application/octet-stream` | | `mp3` | — | MP3 file | 8000–48000 Hz | `32k`–`192k` | `audio/mpeg` | | `ogg` | — | OGG Opus file | 8000–48000 Hz | — | `audio/ogg` | | `mulaw` | — | Raw G.711 µ-law | **forced 8000 Hz** | — | `audio/basic` | | `alaw` | — | Raw G.711 A-law | **forced 8000 Hz** | — | `audio/alaw` | **Encoding aliases** — produce identical output to the `container` rows above: | `container` | `encoding` | Equivalent to | | -------------- | ----------- | ----------------- | | `raw` | `pcm_mulaw` | `container=mulaw` | | `raw` | `pcm_alaw` | `container=alaw` | | `raw` or `ogg` | `oggopus` | `container=ogg` | `bitrate` only applies when `container=mp3`. `container=mulaw`/`alaw` override `sample_rate` to 8000 Hz. *** ## Response A successful request returns `200 OK` with raw binary audio in the format specified by `audio_config.container`. | Status | Meaning | | :----- | :---------------------------------------------------- | | `200` | Audio returned as binary in the requested format | | `400` | Bad request — invalid parameters or unsupported voice | | `429` | Rate limit exceeded | | `500` | Internal server error — retry with backoff | | `503` | Service temporarily unavailable | *** ## Code Example ```bash cURL theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/inference \ -H "Content-Type: application/json" \ -H "X-API-Key-ID: " \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" } }' \ --output response.wav ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.vachana.ai/api/v1/tts/inference", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key-ID": "", }, body: JSON.stringify({ text: "नमस्ते, आप कैसे हैं?", voice: "Nalini", model: "timbre-v2.5", language: "hi-IN", speed: 1.0, audio_config: { sample_rate: 48000, num_channels: 1, sample_width: 2, encoding: "linear_pcm", container: "wav", }, }), }); const audio = await response.arrayBuffer(); console.log("Received audio:", audio.byteLength, "bytes"); ``` ```python Python theme={null} import requests response = requests.post( "https://api.vachana.ai/api/v1/tts/inference", headers={ "Content-Type": "application/json", "X-API-Key-ID": "", }, json={ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav", }, }, ) with open("response.wav", "wb") as f: f.write(response.content) ``` *** ## Available Voices 42 voices across 10 Indian languages, English, and Hinglish. See the full [Voice Catalog](/api/TTS/available-voices) for preferred voices by language, gender, persona, and description. Popular examples: `Nalini` (Hindi), `Kaveri` (English), `Deepak` (Hindi), `Poorvi` (Hinglish). *** ## Python SDK The official Python SDK handles authentication headers and binary audio responses automatically. ### Installation ```bash theme={null} pip install gnani-vachana ``` Requires **Python 3.10+**. ### Authentication ```python Constructor argument theme={null} from gnani.tts import GnaniTTSClient client = GnaniTTSClient(api_key="your-api-key") ``` ```bash Environment variable theme={null} export GNANI_API_KEY="your-api-key" ``` ```python Environment variable (usage) theme={null} from gnani.tts import GnaniTTSClient client = GnaniTTSClient() ``` ### Synthesize Speech ```python theme={null} from gnani.tts import GnaniTTSClient client = GnaniTTSClient(api_key="your-api-key") audio = client.synthesize( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", ) with open("output.wav", "wb") as f: f.write(audio) ``` ### Custom Audio Config ```python theme={null} from gnani.tts import GnaniTTSClient, AudioConfig client = GnaniTTSClient(api_key="your-api-key") audio = client.synthesize( "यह एक टेस्ट है", voice="Nalini", model="timbre-v2.5", language="hi-IN", audio_config=AudioConfig( sample_rate=48000, encoding="linear_pcm", container="wav", ), ) with open("output.wav", "wb") as f: f.write(audio) ``` *** # Text Normalization Guide Source: https://docs.gnani.ai/api/TTS/tts-input-formating How the API automatically converts numbers, currency, dates, IDs, and more into natural spoken form. ## Overview Text normalization (TN) is an automatic step that runs on every synthesis request before audio is generated. It converts written text — numbers, currency, dates, abbreviations, IDs, and more — into the natural spoken form that gets sent to the voice. You don't need to pre-format most input. Write text naturally and TN handles it. Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. Passing numbers, IDs, dates, or currency as raw strings is now handled automatically. The guide below documents exactly what gets converted and how, so you can predict the output and catch edge cases. *** ## Supported Languages TN is active for all 10 supported languages. Select the language using the `language` parameter, or pass `auto` to detect it from the input script. | `language` value | Language | | :--------------- | :-------------------------------------------- | | `auto` | Auto-detected from the input script (default) | | `hi` | Hindi | | `en` | English | | `mr` | Marathi | | `ta` | Tamil | | `te` | Telugu | | `kn` | Kannada | | `ml` | Malayalam | | `gu` | Gujarati | | `pa` | Punjabi | | `bn` | Bengali | English, Hindi, Marathi, and Tamil have the deepest coverage, including irregular number forms and compound constructions. The other six languages are fully supported — number and currency word choices are under continuous refinement, so occasional word-choice updates may arrive over time even though sentence structure is always correct. `hinglish` is **not** a valid `language` value. It appears only as an internal outcome of `auto` detection on code-mixed Devanagari + Latin text. Passing `language: "hinglish"` in a request is rejected. ### How `auto` detection works When `language` is `auto`, the dominant script in your input determines the normalization profile: | Script detected | Resolves to | | :----------------------- | :--------------------------------------- | | Devanagari | Hindi (pass `mr` explicitly for Marathi) | | Bengali script | Bengali | | Gurmukhi | Punjabi | | Gujarati script | Gujarati | | Tamil script | Tamil | | Telugu script | Telugu | | Kannada script | Kannada | | Malayalam script | Malayalam | | Devanagari + Latin mixed | Hinglish (code-mixed) | | No Indic script | English | `auto` cannot select a non-English profile for text that contains no Indic script. A message that's entirely in Latin letters and digits (e.g. `"42"`) always normalizes as English under `auto`, even when synthesized in a non-English voice. Pass the language hint explicitly (e.g. `"kn"`) if you need native-language number reading for Latin-script input. ### English words that always stay in English Regardless of the resolved language, these are always read in English — matching how they're spoken even in fluent Indic speech: * Units of measurement (`kg`, `km`, `°C`, `GB`, etc.) * `AM` / `PM` * URLs and email addresses The words **"point"** (decimals) and **"percent"** stay in English when `language` is `auto`. When you set a language explicitly, they localize — Hindi uses the formal register (दशमलव / प्रतिशत); all other languages use the colloquial loanword form (e.g. Kannada ಪಾಯಿಂಟ್ / ಪರ್ಸೆಂಟ್). *** ## How Normalization Works Every synthesis request passes through the same ordered pipeline: 1. Inline markup (pause or speed tags) is stripped from the spoken output. 2. The target language profile is resolved from `language`, or auto-detected. 3. Honorifics are expanded (`Dr.` → Doctor / डॉक्टर). 4. Address and business abbreviations are expanded (`Rd` → Road, `Ltd` → Limited). 5. Numbers, currency, dates, times, and identifiers are normalized in a fixed rule order — more specific patterns (like a PAN number) are always claimed before general ones (like a bare digit run). 6. Common initialisms (`OTP`, `GST`, `EMI`, etc.) are spelled out letter-by-letter. 7. Natural pause commas are inserted for dense ID and phone patterns. *** ## Numbers | Input | English | Hindi | Notes | | :--------- | :---------------------- | :------------------------------------------------------ | :-------------------------------------------------------------------------------------------- | | `42` | forty two | बयालीस | Cardinal, localized per language | | `-42` | negative forty two | minus बयालीस | English says "negative"; all other languages use the English word "minus" (not yet localized) | | `12.5` | twelve point five | बारह दशमलव पाँच *(explicit)* / बारह point पाँच *(auto)* | "point" only localizes when language is set explicitly | | `10,000` | ten thousand | दस हजार | Western grouping, read as one number | | `2,40,000` | two lakh forty thousand | दो लाख चालीस हजार | Indian lakh-grouping is used for all languages including English | | `5th` | fifth | पाँचवां | English ordinal suffix triggers native ordinal form | ### Cardinal numbers vs. digit-by-digit IDs A bare number isn't always read as a quantity — context decides: * **Quantity** — if the number follows a word like `iPhone`, `Class`, `Sector`, `Seat`, `Row`, or `Gate`, it's always read as a number (e.g. "iPhone fifteen", "Sector eleven"). * **Digit-by-digit** — triggered by context words like `account`, `OTP`, `card`, `PIN`, `Aadhaar`, or `PAN` near the number, or a run of 6+ digits, or exactly 4/6/10/12/16 digits. * **Duration words** (`minutes`, `hours`, `days`) always force a quantity reading, even for digit runs that would otherwise look ID-like. Only English compresses repeated digits when spelling IDs (`880044` → "double eight double zero double four"). Every other language spells each digit individually. ### Numeric ranges An en-dash (–) between two numbers is read as "to": | Input | English output | | :---------- | :----------------------------------------- | | `3–5` | three to five | | `5.7–6.4%` | five point seven to six point four percent | | `25–29 Jun` | twenty five to twenty nine June | | `8–10 AM` | eight to ten A M | *** ## Currency | Input | English output | Notes | | :------------------- | :-------------------------------------------------------- | :----------------------------------------------- | | `₹599` | five hundred ninety nine rupees | | | `₹2,50,000` | two lakh fifty thousand rupees | Indian lakh/crore grouping honored | | `₹4,312.50` | four thousand three hundred twelve rupees and fifty paise | Decimal reads as rupees + paise | | `₹12.6 crore` | twelve point six crore rupees | Decimal reads as "point" before a magnitude word | | `Rs. 500` / `Rs 500` | five hundred rupees | Reads identically to ₹500 | | `$4.7 million` | four point seven million dollars | | The rupee word localizes per language (रुपये in Hindi/Marathi, ரூபாய் in Tamil, etc.). Any currency symbol (`$`, `£`, `¥`, `€`, `¢`) is always expanded to a word — raw symbols never appear in the spoken output. *** ## Percentages | Input | English | Hindi (explicit) | Kannada (explicit) | | :---------- | :--------------------------------- | :-------------------- | :-------------------------------------------- | | `8.5%` | eight point five percent | आठ दशमलव पाँच प्रतिशत | ಎಂಟು ಪಾಯಿಂಟ್ ಐದು ಪರ್ಸೆಂಟ್ | | `7.5% p.a.` | seven point five percent per annum | — | `per annum` stays in English in all languages | *** ## Dates Dates in `DD/MM/YYYY`, `DD-MM-YYYY`, and written-month formats (`25 Jun 2026`) are read in full and localized per language. ### Financial year and quarter references | Input | English output | | :------------- | :------------------------------------------------------------------ | | `FY2024-25` | financial year twenty twenty four twenty twenty five | | `FY2027` | financial year twenty twenty seven | | `Q1` | quarter one | | `Q1 FY2024-25` | quarter one of financial year twenty twenty four twenty twenty five | A two-part number like `23/06` is read as a fraction, not a date. Include the year (`23/06/2026`) if you mean a date. *** ## Time | Format | Example | English output | Notes | | :----------------- | :--------------- | :------------------------------------------- | :----------------------------------------------- | | H:MM with AM/PM | `5:30 PM` | five thirty P M | AM/PM is always spelled as letters | | 24-hour (0, 13–23) | `20:00` | eight P M | Converted to natural 12-hour with AM/PM inferred | | H:MM, no AM/PM | `5:30` | five thirty | AM/PM not guessed when ambiguous | | H:MM:SS | `1:30:45` | one thirty and forty five seconds | | | H:MM:SS AM/PM | `11:59:59 PM` | eleven fifty nine and fifty nine seconds P M | | | H:MM–H:MM range | `10:00–11:30 AM` | ten and eleven thirty A M | | | Bare H AM/PM | `5 PM` | five P M | No colon needed | | HH:MM hrs | `22:00 hrs` | ten P M | | Non-English languages use their own natural time phrasing rather than a direct translation of the English pattern. *** ## Math Symbols | Symbol | Reads as | Example | | :------------ | :----------------------------------------------- | :----------------------------------- | | `+` | plus | `2 + 3` → two plus three | | `=` | is equal to | `= 5` → is equal to five | | `×`, `x`, `*` | multiplied by | `2 x 3` → two multiplied by three | | `÷` | divided by | `10 ÷ 2` → ten divided by two | | `√` | square root of | `√64` → square root of sixty four | | `±` | plus or minus | `5 ± 2` → five plus or minus two | | `<` / `>` | less than / greater than | `>90` → greater than ninety | | `≤` / `≥` | less than or equal to / greater than or equal to | `≥10` → greater than or equal to ten | | `²` / `³` | squared / cubed | `5²` → five squared | | `½` `¼` `¾` | and a half / and a quarter / and three quarters | `2½` → two and a half | When `×`, `x`, or `*` is immediately followed by a unit (`inches`, `cm`, `ft`), it reads as "by" instead of "multiplied by" — `5x7 inches` → "five by seven inches." The fixed idiom `24x7`, `24×7`, and `24/7` always reads as "twenty four seven." *** ## Fractions A bare `/` that isn't part of a recognized date reads as a fraction: | Input | English | Hindi | Marathi | | :------ | :------------------ | :-------- | :------------ | | `1/2` | one by two | एक बटा दो | एक भागिले दोन | | `23/06` | twenty three by six | — | — | Only Hindi and Marathi have a confirmed native connector word ("बटा" / "भागिले"). The other seven languages use the English loanword "by." *** ## Units & Measurements | Unit | Reads as | Notes | | :----------------- | :----------------------------------- | :------------------------------------------------------------------------ | | `kg` | kilograms | any case | | `g` | grams | lowercase only — uppercase `G` after a digit is the 4G/5G telecom pattern | | `km` | kilometres | any case | | `mg` | milligrams | lowercase only — avoids misreading "123 MG Road" as a dosage | | `mcg` | micrograms | any case | | `ml` / `mL` | millilitres | any case | | `L` | litres | any case | | `m` | metres | any case | | `ms` | milliseconds | any case | | `°C` / `°F` | degrees Celsius / degrees Fahrenheit | | | `kcal` | kilocalories | any case | | `GB` / `MB` / `TB` | gigabytes / megabytes / terabytes | any case | | `Mbps` / `MBps` | megabits/megabytes per second | **exact case only** — any other casing is left unexpanded | | `Kbps` | kilobits per second | any case | | `km/h` / `kmph` | kilometres per hour | any case | | `sqft` | square feet | any case | | Notation | Reads as | Example | | :------------------ | :-------------------------- | :----------------------------------------------- | | `5'11''` or `5'11"` | feet + inches | five feet eleven inches | | `12°N, 77°E` | degrees + compass direction | twelve degrees North, seventy seven degrees East | | `₹4,500/month` | per + period | …rupees per month | | `₹599/28 days` | per + count + period | …rupees per twenty eight days | A bare `5°` with no unit letter (`C`/`F`) or compass direction (`N`/`S`/`E`/`W`) is not normalized. Always include the unit so temperatures and coordinates read correctly. *** ## Roman Numerals Roman numerals are only converted in two cases: 1. The entire input is a roman numeral by itself (`XIV` → fourteen). 2. A roman numeral immediately follows a trigger word: `Chapter`, `Part`, `Section`, `Volume`, `Book`, `Class`, `Grade`, `King`, `Queen`, `Pope`, `Emperor`, `World War`, `Super Bowl`, `Article`, or `Appendix` (e.g. "Chapter XIV" → "Chapter fourteenth"). Ordinary words that happen to look like roman numerals (`MIX`, `CIVIC`, `MD`) are never converted in normal sentences. *** ## Identifiers Structured IDs are read with grouped digits/letters and natural pauses between groups. | Entity | Example input | Readout pattern | | :----------------------- | :------------------------- | :------------------------------------------------------------------------------------------------------------ | | PAN | `ABCDE1234F` | Letters, then digits, then final letter — each group set off by a pause. Read as the word "pan" (not "P A N") | | Aadhaar | `1234-5678-9012` | Three groups of 4 digits, separated by pauses | | IFSC | `SBIN0001234` | Bank code as one spoken group, then branch code spelled out | | GST / GSTIN | `GST 22ABCDE1234F1Z5` | "G S T" followed by the full code character-by-character | | Phone (10-digit) | `9876543210` | Grouped 3-3-4, comma-separated | | Phone (international) | `+91 98765 43210` | "plus" + country code, then the national number | | Toll-free / landline | `1800-440-1234` | Read in the written digit groups | | OTP | `OTP is 482913` | Digits spelled individually | | PIN | `ATM PIN: 1234` | Digit-by-digit | | Masked number | `****4821` | Mask skipped, only visible digits read | | Account / reference code | `account ABC1234` | Trigger word + code spelled in groups | | Vehicle plate | `DL 1C AF 5031` | Full plate spelled character-by-character | | Train / flight number | `flight AI 101` | Trigger word + code spelled out | | EPIC (voter ID) | `EPIC/WB/8821` | Each segment spelled out, joined by "slash" | | Seat / gate / row | `Seat 15A`, `Gate 14B` | "fifteen A", "fourteen B" | | Invoice / order # | `#12345`, `Invoice #12345` | "Invoice number one two three four five" | | UPI handle | `9876543210@ybl` | Digit local part spelled in chunks; bank suffix spelled letter-by-letter | **Known identifier limitations:** * **4-segment EPIC IDs** (e.g. `EPIC/WB/8821/4521`) can misread — a fraction rule may claim the last two segments. Use the 3-segment form or a non-slash separator until fixed. * **OTP/PIN digits** may be read with "double/triple" compression for repeated digits (e.g. `448822` → "double four double eight double two"). Contact your Gnani representative if you need guaranteed uncompressed digit-by-digit OTP readout. * **Very long reference numbers** (14+ digits) are read as a raw digit string rather than being grouped. Add your own separators (spaces or hyphens) for a more natural readout. *** ## URLs, Emails & UPI Handles | Input | Output | | :------------------------- | :------------------------------------------------------------------- | | `https://abc123.com/path1` | H T T P S colon slash slash abc one two three dot com slash path one | | `www.gov.in/schemes` | W W W dot gov dot in slash schemes | | `ram.kumar99@hdfc.co.in` | Local part spelled in chunks, domain read dot-by-dot | URLs and emails are always read in English regardless of the target language. Digits inside them use English digit words even in non-English speech. *** ## Honorifics | Written | English | Hindi | | :------------------ | :-------- | :-------- | | `Mr.` | Mister | श्री | | `Mrs.` | Missus | श्रीमती | | `Ms.` | Miss | सुश्री | | `Dr.` | Doctor | डॉक्टर | | `Prof.` | Professor | प्रोफ़ेसर | | `Shri` / `Sh.` | Shri | श्री | | `Shrimati` / `Smt.` | Shrimati | श्रीमती | | `Kumari` / `Km.` | Kumari | कुमारी | Every supported language has its own honorific table (e.g. Tamil `Mr.` → திரு, Telugu `Dr.` → డాక్టర్). `Km.` and `Ms.` also look like unit abbreviations (kilometres, milliseconds). When a number directly precedes them (`10 km`, `200 ms`), they read as the unit, not the honorific. This is a deliberate trade-off — real-world text essentially never has a number immediately before a name. *** ## Address & Business Abbreviations These are always read in English, since they're conventionally spoken as English loanwords even in Indic-language speech. | Written | Reads as | Notes | | :------------------------------------- | :----------- | :------------------------------------------------------ | | `St` / `St.` before a comma | Street | `123 Main St, Delhi` → "Main Street, Delhi" | | `St` / `St.` before a capitalized word | Saint | `St. Xavier's` → "Saint Xavier's" | | `Rd` | Road | | | `Ave` | Avenue | | | `Ln` | Lane | | | `Blvd` | Boulevard | | | `Apt` | Apartment | | | `Bldg` | Building | | | `Sec` | Sector | | | `Dist` | District | | | `Opp` | Opposite | | | `Nr` | Near | | | `Pvt` | Private | | | `Ltd` | Limited | | | `Co.` | Company | Only with a trailing period — "Co-founder" is untouched | | `Inc` | Incorporated | | | `Corp` | Corporation | | | `No.` before digits | Number | Only when directly followed by digits | | `(Gen)` | General | Parenthetical form only — "Gen Z" is untouched | *** ## Brand & Product Name Pronunciation Some brand names don't have a reliable pronunciation from their spelling. As an example, Gnani is rendered natively per language rather than read literally: | Language | Pronunciation | | :-------------- | :------------ | | English | Gyaanee | | Hindi / Marathi | ज्ञानी | | Kannada | ಜ್ಞಾನಿ | | Telugu | జ్ఞాని | | Malayalam | ജ്ഞാനി | | Tamil | ஞானி | | Gujarati | જ્ઞાની | | Punjabi | ਗਿਆਨੀ | | Bengali | জ্ঞানী | This only applies to the standalone word — "Gnani" inside an email address is read as a normal email, not substituted. If a brand or product name in your content consistently misreads, contact your Gnani point of contact — custom pronunciation entries can be added. *** ## Common Abbreviations These initialisms are always spelled letter-by-letter: OTP, EMI, UPI, NEFT, RTGS, NACH, IMPS, ATM, KYC, GST, GSTIN, IFSC, SMS, IVR, API, URL, AI, ML, GPU, CPU, VPN, HTML, CSS, XML, OCR, QR, SDK, GB, MB, TB, CV, FY, MD, SBI, FD, PDF, IIT, IIM, NIT, HDFC, ICICI, NRI, JEE, NEET, OTT, GDP, FIR, TAN, TIN, DIN, COO, USB, LED, LCD, TV, DNA, CVV, UAN, EPF, NGO, CEO, CFO, CTO, AIIMS, UPSC, IAS, IPS, RTO, PPF, NPS, DTH, NOC, HSN A smaller set is read as a word instead of being spelled out: `PIN`, `SIM`, `RAM`, `ROM`, `AIDS`, `SEBI`, `TRAI`, `CIBIL`, `RERA`, `AMFI`. `Govt` expands to "Government." `Aadhaar` is always kept as-is. Devanagari-spelled Latin abbreviations are recognized the same way — e.g. `केवाईसी` expands to "के वाई सी" (KYC). *** ## Telecom Generation | Input | Output | | :---- | :----- | | `5G` | five G | | `4G` | four G | Scoped to 2G–5G so it's never misread as a measurement (e.g. "five grams"). *** ## Duration Phrases | Input | Output | | :----------- | :-------------- | | `15 minutes` | fifteen minutes | | `2 hours` | two hours | | `3 days` | three days | Abbreviated units expand to their full word (`hrs` → "hours", `min` → "minutes", `sec` → "seconds"). Numbers near a duration word are always read as a quantity, even if they'd otherwise look like an ID. The singular `hr` (e.g. "1 hr later") is **not** covered — `HR` is reserved for "Human Resources." Use the full word `hour` or the plural `hrs` for duration. *** ## Natural Pauses Beyond punctuation already in your text, pause commas are automatically inserted for dense identifier patterns — PAN numbers, Aadhaar, OTPs near long digit runs, IFSC near a bank code, or international phone numbers with `+91`. This prevents multiple codes from running together. Outside these specific patterns, extra commas are not added. Simpler sentences also have excess commas trimmed so the readout doesn't sound choppy. *** ## Known Limitations | Limitation | Workaround | | :------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | | Two-part dates like `23/06` read as a fraction | Include the year: `23/06/2026` | | `5°` with no unit letter isn't normalized | Write `5°C`, `5°F`, or a compass direction | | 4-segment EPIC IDs can misread | Use the 3-segment form or a non-slash separator | | Very long reference numbers (14+ digits) read as raw digit strings | Add spaces or hyphens between groups | | Singular `hr` reads as "H R", not "hour" | Use `hour` or `hrs` | | `minus` is not localized — every language uses the English word | No workaround yet | | Repeated OTP/PIN digits may be compressed ("double four" instead of "four four") | Contact your Gnani representative if strict digit-by-digit readout is required | | `Mbps`/`MBps` are only guaranteed correct in their exact documented casing | Use the exact case shown; other casings are left unexpanded | | `auto` detection with Latin-script-only input always resolves to English | Pass the language hint explicitly (e.g. `"kn"`) | | Fraction connector word "by" only has a native form in Hindi and Marathi | Other languages use the English loanword | # Text-to-Speech (Streaming) Source: https://docs.gnani.ai/api/TTS/tts-sse POST /api/v1/tts/sse Stream audio in chunks as it's generated via Server-Sent Events. ## Overview Receive audio in chunks as it's generated, allowing playback to start immediately. Reduces latency compared to [TTS REST](/api/TTS/tts-inference). For the lowest latency, see [TTS Realtime](/api/TTS/tts-websocket). Passing numbers, IDs, dates, or currency as raw strings causes mispronunciations. See the [Input Formatting Guide](/api/TTS/tts-input-formating) for correct formatting of phone numbers, account numbers, PINs, Aadhaar, vehicle registration numbers, GSTIN, currency, and more. *** ## Models | Model | `model` value | Languages | Extra parameters | | :---------- | :------------ | :------------------ | :------------------ | | Timbre v2.5 | `timbre-v2.5` | 10 Indian languages | `language`, `speed` | Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. *** ## Endpoint ```text theme={null} POST https://api.vachana.ai/api/v1/tts/sse ``` *** ## Authentication | Header | Required | Description | | :------------- | :------- | :----------------- | | `X-API-Key-ID` | Yes | Your Gnani API key | *** ## Request Parameters ```json theme={null} { "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" } } ``` The text to synthesize into speech. Pass numbers, dates, and currency as spoken words to avoid mispronunciations — see [Input Formatting Guide](/api/TTS/tts-input-formating). Set to `timbre-v2.5`. Voice name from the Timbre v2.5 catalog. See the [Voice Catalog](/api/TTS/available-voices) for preferred voices by language. Example: `Nalini`, `Kaveri`, `Deepak`. Language of the input text. Controls text normalization and routing. | Value | Language | | :------ | :------------------------------------------------------ | | `auto` | Auto-detect from script (recommended for mixed content) | | `hi-IN` | Hindi | | `en-IN` | English | | `ta-IN` | Tamil | | `te-IN` | Telugu | | `kn-IN` | Kannada | | `ml-IN` | Malayalam | | `mr-IN` | Marathi | | `pa-IN` | Punjabi | | `bn-IN` | Bengali | | `gu-IN` | Gujarati | | `hi-en` | Hinglish (code-mixed Hindi–English) | Playback speed multiplier. Numeric range: `0.85` (slowest) to `1.15` (fastest). String shortcuts: `"slow"` = 0.85, `"medium"` = 1.0, `"fast"` = 1.15. Controls the format of the returned audio. Sample rate in Hz. Supported: `8000`, `16000`, `22050`, `24000`, `44100`, `48000`. Number of audio channels. `1` for mono. Sample width in bytes. `2` for 16-bit audio. Audio encoding format. Options: `linear_pcm`, `pcm_s16le`, `pcm_mulaw`, `pcm_alaw`, `oggopus`. For telephony, prefer `container=mulaw` or `container=alaw` over this field — both produce the same output. Use `oggopus` (or `container=ogg`) for a playable OGG Opus file. Not required when `container` is `mp3`. Output container format. Options: `wav`, `raw`, `mp3`, `ogg`, `mulaw`, `alaw`. Use `ogg` for OGG Opus. Use `mulaw` or `alaw` for G.711 telephony (forces 8000 Hz). MP3 bitrate. Only used when `container` is `mp3`. Supported: `32k`, `64k`, `96k`, `128k`, `192k`. *** ## Audio Format Reference | `container` | `encoding` | Output | `sample_rate` | `bitrate` | Content-Type | | ----------- | ------------ | ---------------------- | ------------------ | ------------ | -------------------------- | | `wav` | `linear_pcm` | WAV file (with header) | 8000–48000 Hz | — | `audio/wav` | | `raw` | `linear_pcm` | Raw 16-bit PCM | 8000–48000 Hz | — | `application/octet-stream` | | `mp3` | — | MP3 file | 8000–48000 Hz | `32k`–`192k` | `audio/mpeg` | | `ogg` | — | OGG Opus file | 8000–48000 Hz | — | `audio/ogg` | | `mulaw` | — | Raw G.711 µ-law | **forced 8000 Hz** | — | `audio/basic` | | `alaw` | — | Raw G.711 A-law | **forced 8000 Hz** | — | `audio/alaw` | **Encoding aliases** — produce identical output to the `container` rows above: | `container` | `encoding` | Equivalent to | | -------------- | ----------- | ----------------- | | `raw` | `pcm_mulaw` | `container=mulaw` | | `raw` | `pcm_alaw` | `container=alaw` | | `raw` or `ogg` | `oggopus` | `container=ogg` | `bitrate` only applies when `container=mp3`. `container=mulaw`/`alaw` override `sample_rate` to 8000 Hz. When `container=ogg` or `encoding=oggopus` is requested, all audio chunks are encoded into a single OGG Opus file delivered as one chunk — streaming delivery is not possible for OGG. *** ## Response The server streams audio via Server-Sent Events. Each `chunk` event carries base64-encoded audio in the `audio` field. A final `complete` event signals that synthesis is finished. ```text theme={null} event: start data: {"status": "streaming_started", "text": "नमस्ते, आप कैसे हैं?"} event: chunk data: {"chunk_index": 1, "audio": "", "is_final": false} event: chunk data: {"chunk_index": 2, "audio": "", "is_final": false} event: complete data: {"chunk_index": 3, "audio": "", "is_final": true} ``` | Event | Description | | :--------- | :-------------------------------------------------------------------------------- | | `start` | Synthesis has begun. | | `chunk` | An audio fragment. Decode `audio` from base64 and append to your buffer in order. | | `complete` | All audio has been sent. `is_final: true`, `audio` is empty. | | `error` | Synthesis failed. Contains error details when returned by the server. | *** ## Code Example ```bash cURL theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/sse \ -H "Content-Type: application/json" \ -H "X-API-Key-ID: " \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "encoding": "linear_pcm", "container": "wav" } }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.vachana.ai/api/v1/tts/sse", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key-ID": "", }, body: JSON.stringify({ text: "नमस्ते, आप कैसे हैं?", voice: "Nalini", model: "timbre-v2.5", language: "hi-IN", speed: 1.0, audio_config: { sample_rate: 48000, encoding: "linear_pcm", container: "wav", }, }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log(decoder.decode(value)); } ``` ```python Python theme={null} import requests, base64 response = requests.post( "https://api.vachana.ai/api/v1/tts/sse", headers={ "Content-Type": "application/json", "X-API-Key-ID": "", }, json={ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "encoding": "linear_pcm", "container": "wav", }, }, stream=True, ) for line in response.iter_lines(): if line: print(line.decode("utf-8")) ``` *** ## Available Voices 42 voices across 10 Indian languages, English, and Hinglish. See the full [Voice Catalog](/api/TTS/available-voices) for preferred voices by language, gender, persona, and description. Popular examples: `Nalini` (Hindi), `Kaveri` (English), `Deepak` (Hindi), `Poorvi` (Hinglish). *** ## Python SDK The SDK's streaming client handles SSE parsing and chunk reassembly for you — you just iterate and write. ### Installation ```bash theme={null} pip install gnani-vachana ``` Requires **Python 3.10+**. ### Authentication ```python Constructor argument theme={null} from gnani.tts import GnaniTTSStreamClient client = GnaniTTSStreamClient(api_key="your-api-key") ``` ```bash Environment variable theme={null} export GNANI_API_KEY="your-api-key" ``` ```python Environment variable (usage) theme={null} from gnani.tts import GnaniTTSStreamClient client = GnaniTTSStreamClient() ``` ### Stream Audio to a File `synthesize_stream` yields audio chunks as they arrive. Playback or writing can begin before the full response is complete. ```python theme={null} from gnani.tts import GnaniTTSStreamClient client = GnaniTTSStreamClient(api_key="your-api-key") with open("output.wav", "wb") as f: for chunk in client.synthesize_stream( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", ): f.write(chunk) ``` ### With Custom Audio Config ```python theme={null} from gnani.tts import GnaniTTSStreamClient, AudioConfig client = GnaniTTSStreamClient(api_key="your-api-key") with open("output.wav", "wb") as f: for chunk in client.synthesize_stream( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", audio_config=AudioConfig( sample_rate=48000, encoding="linear_pcm", container="wav", ), ): f.write(chunk) ``` # Text-to-Speech (Realtime) Source: https://docs.gnani.ai/api/TTS/tts-websocket Real-time text-to-speech with streaming audio via WebSocket. ## Overview Stream audio in real-time with the lowest latency. Perfect for interactive assistants and live applications. | Use case | Recommended endpoint | | :-------------------------------------------- | :-------------------------------------- | | Lowest-latency interactive / live apps | **This endpoint** | | Full audio in one response (downloads, batch) | [TTS REST](/api/TTS/tts-inference) | | Chunked playback as audio is generated | [TTS Streaming (SSE)](/api/TTS/tts-sse) | Passing numbers, IDs, dates, or currency as raw strings causes mispronunciations. See the [Input Formatting Guide](/api/TTS/tts-input-formating) for correct formatting of phone numbers, account numbers, PINs, Aadhaar, vehicle registration numbers, GSTIN, currency, and more. *** ## Models | Model | `model` value | Languages | Extra parameters | | :---------- | :------------ | :------------------ | :------------------ | | Timbre v2.5 | `timbre-v2.5` | 10 Indian languages | `language`, `speed` | Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. *** ## Endpoint ```text theme={null} WSS wss://api.vachana.ai/api/v1/tts ``` *** ## Connection Headers Pass all configuration as WebSocket upgrade headers. Headers cannot be changed mid-session — reconnect with new headers to change settings. | Header | Required | Description | | :------------- | :------- | :----------------- | | `X-API-Key-ID` | Yes | Your Gnani API key | *** ## Connection Flow 1. **Client connects** — opens a WebSocket to `/api/v1/tts` with all required headers. 2. **Client sends a synthesis message** — JSON with `text`, `voice`, `model`, and `audio_config`. 3. **Server streams audio** — returns base64-encoded audio chunks as JSON messages. 4. **Server completes** — sends a final message with `is_final: true`. The client may send another request or close. 5. **Either side closes** — client or server may close the connection at any time. Send one synthesis request at a time and consume audio chunks as they arrive for the lowest end-to-end latency. *** ## Request Parameters ```json theme={null} { "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" } } ``` The text to synthesize into speech. Pass numbers, dates, and currency as spoken words to avoid mispronunciations — see [Input Formatting Guide](/api/TTS/tts-input-formating). Set to `timbre-v2.5`. Voice name from the Timbre v2.5 catalog. See the [Voice Catalog](/api/TTS/available-voices) for preferred voices by language. Example: `Nalini`, `Kaveri`, `Deepak`. Language of the input text. Controls text normalization and routing. | Value | Language | | :------ | :------------------------------------------------------ | | `auto` | Auto-detect from script (recommended for mixed content) | | `hi-IN` | Hindi | | `en-IN` | English | | `ta-IN` | Tamil | | `te-IN` | Telugu | | `kn-IN` | Kannada | | `ml-IN` | Malayalam | | `mr-IN` | Marathi | | `pa-IN` | Punjabi | | `bn-IN` | Bengali | | `gu-IN` | Gujarati | | `hi-en` | Hinglish (code-mixed Hindi–English) | Playback speed multiplier. Numeric range: `0.85` (slowest) to `1.15` (fastest). String shortcuts: `"slow"` = 0.85, `"medium"` = 1.0, `"fast"` = 1.15. Controls the format of the streamed audio chunks. Sample rate in Hz. Supported: `8000`, `16000`, `22050`, `24000`, `44100`, `48000`. Number of audio channels. `1` for mono. Sample width in bytes. `2` for 16-bit audio. Audio encoding format. Options: `linear_pcm`, `pcm_s16le`, `pcm_mulaw`, `pcm_alaw`, `oggopus`. For telephony, prefer `container=mulaw` or `container=alaw` over this field — both produce the same output. Use `oggopus` (or `container=ogg`) for a playable OGG Opus file. Not required when `container` is `mp3`. Output container format. Options: `wav`, `raw`, `mp3`, `ogg`, `mulaw`, `alaw`. Use `ogg` for OGG Opus. Use `mulaw` or `alaw` for G.711 telephony (forces 8000 Hz). MP3 bitrate. Only used when `container` is `mp3`. Supported: `32k`, `64k`, `96k`, `128k`, `192k`. *** ## Audio Format Reference | `container` | `encoding` | Output | `sample_rate` | `bitrate` | Content-Type | | ----------- | ------------ | ---------------------- | ------------------ | ------------ | -------------------------- | | `wav` | `linear_pcm` | WAV file (with header) | 8000–48000 Hz | — | `audio/wav` | | `raw` | `linear_pcm` | Raw 16-bit PCM | 8000–48000 Hz | — | `application/octet-stream` | | `mp3` | — | MP3 file | 8000–48000 Hz | `32k`–`192k` | `audio/mpeg` | | `ogg` | — | OGG Opus file | 8000–48000 Hz | — | `audio/ogg` | | `mulaw` | — | Raw G.711 µ-law | **forced 8000 Hz** | — | `audio/basic` | | `alaw` | — | Raw G.711 A-law | **forced 8000 Hz** | — | `audio/alaw` | **Encoding aliases** — produce identical output to the `container` rows above: | `container` | `encoding` | Equivalent to | | -------------- | ----------- | ----------------- | | `raw` | `pcm_mulaw` | `container=mulaw` | | `raw` | `pcm_alaw` | `container=alaw` | | `raw` or `ogg` | `oggopus` | `container=ogg` | `bitrate` only applies when `container=mp3`. `container=mulaw`/`alaw` override `sample_rate` to 8000 Hz. When `container=ogg` or `encoding=oggopus` is requested, all audio is encoded into a single OGG Opus file and delivered as one chunk after synthesis completes. *** ## Server Messages ### `start` Sent when the server begins synthesis for the current request. ```json theme={null} { "type": "start", "message": "Streaming started", "request_id": "req_abc123" } ``` ### Audio chunk Each audio chunk arrives as a JSON message. Decode `audio` from base64 and append chunks in order. ```json theme={null} { "type": "audio", "data": { "chunk_index": 1, "audio": "", "is_final": false } } ``` ### `complete` Sent when all audio for the current request has been streamed. `is_final` is `true` and `audio` is empty. ```json theme={null} { "type": "complete", "data": { "chunk_index": 12, "audio": "", "is_final": true } } ``` ### `error` Sent when the server encounters an error. The connection remains open after recoverable errors. ```json theme={null} { "type": "error", "message": "Invalid text or audio configuration." } ``` *** ## Code Example ```javascript JavaScript theme={null} const ws = new WebSocket("wss://api.vachana.ai/api/v1/tts", { headers: { "Content-Type": "application/json", "X-API-Key-ID": "", }, }); ws.on("open", () => { ws.send(JSON.stringify({ text: "नमस्ते, आप कैसे हैं?", voice: "Nalini", model: "timbre-v2.5", language: "hi-IN", speed: 1.0, audio_config: { sample_rate: 48000, num_channels: 1, sample_width: 2, encoding: "linear_pcm", container: "wav", }, })); }); ws.on("message", (data) => { const msg = JSON.parse(data); if (msg.type === "audio") { const chunk = Buffer.from(msg.data.audio, "base64"); // append chunk to your audio buffer } }); ws.on("error", (error) => console.error("WebSocket error:", error)); ws.on("close", () => console.log("Connection closed")); ``` ```python Python theme={null} import websocket, json, base64 def on_message(ws, message): msg = json.loads(message) if msg.get("type") == "audio": chunk = base64.b64decode(msg["data"]["audio"]) # append chunk to your audio buffer def on_error(ws, error): print(f"Error: {error}") def on_close(ws, close_status_code, close_msg): print("Connection closed") def on_open(ws): ws.send(json.dumps({ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav", }, })) ws = websocket.WebSocketApp( "wss://api.vachana.ai/api/v1/tts", header={ "Content-Type": "application/json", "X-API-Key-ID": "", }, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close, ) ws.run_forever() ``` *** ## Available Voices 42 voices across 10 Indian languages, English, and Hinglish. See the full [Voice Catalog](/api/TTS/available-voices) for preferred voices by language, gender, persona, and description. Popular examples: `Nalini` (Hindi), `Kaveri` (English), `Deepak` (Hindi), `Poorvi` (Hinglish). *** ## Python SDK The SDK's realtime client manages the WebSocket lifecycle and async iteration so you can focus on your application logic. ### Installation ```bash theme={null} pip install gnani-vachana ``` Requires **Python 3.10+**. ### Authentication ```python Constructor argument theme={null} from gnani.tts import GnaniTTSRealtimeClient client = GnaniTTSRealtimeClient(api_key="your-api-key") ``` ```bash Environment variable theme={null} export GNANI_API_KEY="your-api-key" ``` ```python Environment variable (usage) theme={null} from gnani.tts import GnaniTTSRealtimeClient client = GnaniTTSRealtimeClient() ``` ### Stream Audio Chunks in Real-Time ```python theme={null} import asyncio from gnani.tts import GnaniTTSRealtimeClient async def main(): async with GnaniTTSRealtimeClient(api_key="your-api-key") as client: with open("output.wav", "wb") as f: async for chunk in client.synthesize( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", ): f.write(chunk) asyncio.run(main()) ``` ### Collect All Audio at Once ```python theme={null} import asyncio from gnani.tts import GnaniTTSRealtimeClient async def main(): async with GnaniTTSRealtimeClient(api_key="your-api-key") as client: audio = await client.synthesize_and_collect( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", ) with open("output.wav", "wb") as f: f.write(audio) asyncio.run(main()) ``` *** # Voice Cloned TTS (REST) Source: https://docs.gnani.ai/api/VC/vc-inference VC.yml POST /api/v1/tts/inference Generate cloned voice audio in a single synchronous response. ## Overview Upload a reference audio clip to get a `speaker_embedding`. See [Voice Clone Embeddings](/api/VC/voice-clone-embeddings). Pass the `speaker_embedding` from Step 1 to this endpoint to synthesize audio in your cloned voice. Synthesize audio using your cloned voice. Pass the `speaker_embedding` obtained from [Voice Clone Embeddings](/api/VC/voice-clone-embeddings) along with your text. The full audio is returned in one response. For streaming playback, see [Voice Cloning Streaming](/api/VC/vc-sse) or [Voice Cloning Realtime](/api/VC/vc-websocket). ## Endpoint ``` POST https://api.vachana.ai/api/v1/tts/inference ``` ## Authentication | Header | Required | Description | Example | | -------------- | -------- | ------------------------------- | ------------------ | | `X-API-Key-ID` | Yes | Your API key for authentication | `your-api-key-id` | | `Content-Type` | Yes | Must be `application/json` | `application/json` | ## Request Body The text to synthesize into speech Voice cloning model. Use `vachana-vc-v1`. Automatically set when `speaker_embedding` is provided — you may omit this field. Audio output configuration Sample rate in Hz (8000-44100) Number of audio channels (1-8) Sample width in bytes (1-4) Audio encoding format: `linear_pcm` or `oggopus` Audio container format: `raw`, `mp3`, `wav`, `mulaw`, or `ogg` MP3 bitrate (only when container=mp3): `96k`, `128k`, or `192k` Voice clone embedding obtained from the Voice Clone Embeddings endpoint The voice clone embedding string Shape of the embedding tensor, e.g., `[1, 768]` Data type of the embedding, e.g., `torch.bfloat16` ## Response Returns binary audio data in the format specified by `audio_config.container`: * `audio/wav` for WAV files * `audio/mpeg` for MP3 files * `audio/ogg` for OGG files ## Example Request ```bash cURL theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/inference \ -H "X-API-Key-ID: your-api-key-id" \ -H "Content-Type: application/json" \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm", "container": "wav" }, "speaker_embedding": { "embedding": "your-embedding-string", "shape": [1, 768], "dtype": "torch.bfloat16" } }' \ --output audio.wav ``` ```python Python theme={null} import requests url = "https://api.vachana.ai/api/v1/tts/inference" headers = { "X-API-Key-ID": "your-api-key-id", "Content-Type": "application/json" } payload = { "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm", "container": "wav" }, "speaker_embedding": { "embedding": "your-embedding-string", "shape": [1, 768], "dtype": "torch.bfloat16" } } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: with open("audio.wav", "wb") as f: f.write(response.content) print("Audio saved successfully") else: print(f"Error: {response.status_code}") print(response.json()) ``` ```javascript JavaScript theme={null} const url = "https://api.vachana.ai/api/v1/tts/inference"; const headers = { "X-API-Key-ID": "your-api-key-id", "Content-Type": "application/json" }; const payload = { text: "नमस्ते, आप कैसे हैं?", model: "vachana-vc-v1", audio_config: { sample_rate: 44100, encoding: "linear_pcm", container: "wav" }, speaker_embedding: { embedding: "your-embedding-string", shape: [1, 768], dtype: "torch.bfloat16" } }; fetch(url, { method: "POST", headers: headers, body: JSON.stringify(payload) }) .then(response => response.blob()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "audio.wav"; a.click(); }) .catch(error => console.error("Error:", error)); ``` ## Error Responses Invalid text or audio configuration ```json theme={null} { "success": false, "error": { "type": "INVALID_REQUEST_ERROR", "message": "Invalid text or audio configuration." } } ``` Rate limit exceeded ```json theme={null} { "success": false, "error": { "type": "RATE_LIMIT_ERROR", "message": "Rate limit exceeded. Please try again later." } } ``` Unexpected error occurred ```json theme={null} { "success": false, "error": { "type": "API_ERROR", "message": "An unexpected error occurred while processing." } } ``` # Voice Cloned TTS (Streaming) Source: https://docs.gnani.ai/api/VC/vc-sse VC.yml POST /api/v1/tts/sse Stream cloned voice audio in chunks via Server-Sent Events. ## Overview Upload a reference audio clip to get a `speaker_embedding`. See [Voice Clone Embeddings](/api/VC/voice-clone-embeddings). Pass the `speaker_embedding` from Step 1 to this endpoint to stream cloned voice audio progressively. Stream cloned voice audio as it's generated using Server-Sent Events. Pass the `speaker_embedding` from [Voice Clone Embeddings](/api/VC/voice-clone-embeddings) to use your cloned voice. Reduces latency compared to [Voice Cloned TTS REST](/api/VC/vc-inference). For the lowest latency, see [Voice Cloned TTS Realtime](/api/VC/vc-websocket). ## Endpoint ``` POST https://api.vachana.ai/api/v1/tts/sse ``` ## Authentication | Header | Required | Description | Example | | -------------- | -------- | ------------------------------- | ------------------ | | `X-API-Key-ID` | Yes | Your API key for authentication | `your-api-key-id` | | `Content-Type` | Yes | Must be `application/json` | `application/json` | ## Request Body The text to synthesize into speech Voice cloning model. Use `vachana-vc-v1`. Automatically set when `speaker_embedding` is provided — you may omit this field. Audio output configuration Sample rate in Hz (8000-44100) Number of audio channels (1-8) Sample width in bytes (1-4) Audio encoding format: `linear_pcm` or `oggopus` Audio container format: `raw`, `mp3`, `wav`, `mulaw`, or `ogg` MP3 bitrate (only when container=mp3): `96k`, `128k`, or `192k` Voice clone embedding obtained from the Voice Clone Embeddings endpoint The voice clone embedding string Shape of the embedding tensor, e.g., `[1, 768]` Data type of the embedding, e.g., `torch.bfloat16` ## Response The server streams audio data via Server-Sent Events (SSE). Each event contains a chunk of audio data encoded in base64. ### Event Types Sent when synthesis begins. ``` event: start data: {"status": "streaming_started", "text": "नमस्ते"} ``` Contains base64-encoded audio in the `audio` field. ``` event: chunk data: {"chunk_index": 1, "audio": "", "is_final": false} ``` Signals the end of the audio stream. ``` event: complete data: {"chunk_index": 2, "audio": "", "is_final": true} ``` ## Example Request ```bash cURL theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/sse \ -H "X-API-Key-ID: your-api-key-id" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -N \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm", "container": "wav" }, "speaker_embedding": { "embedding": "your-embedding-string", "shape": [1, 768], "dtype": "torch.bfloat16" } }' ``` ```python Python theme={null} import requests import base64 url = "https://api.vachana.ai/api/v1/tts/sse" headers = { "X-API-Key-ID": "your-api-key-id", "Content-Type": "application/json", "Accept": "text/event-stream" } payload = { "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm", "container": "wav" }, "speaker_embedding": { "embedding": "your-embedding-string", "shape": [1, 768], "dtype": "torch.bfloat16" } } response = requests.post(url, headers=headers, json=payload, stream=True) audio_chunks = [] for line in response.iter_lines(): if line: line = line.decode('utf-8') if line.startswith('data: '): data = line[6:] if data.startswith('{'): # Completed event print("Stream completed") else: # Audio chunk audio_chunks.append(base64.b64decode(data)) # Combine and save audio with open("audio.wav", "wb") as f: for chunk in audio_chunks: f.write(chunk) print("Audio saved successfully") ``` ```javascript JavaScript theme={null} const url = "https://api.vachana.ai/api/v1/tts/sse"; const headers = { "X-API-Key-ID": "your-api-key-id", "Content-Type": "application/json", "Accept": "text/event-stream" }; const payload = { text: "नमस्ते, आप कैसे हैं?", model: "vachana-vc-v1", audio_config: { sample_rate: 44100, encoding: "linear_pcm", container: "wav" }, speaker_embedding: { embedding: "your-embedding-string", shape: [1, 768], dtype: "torch.bfloat16" } }; const eventSource = new EventSource(url); const audioChunks = []; eventSource.addEventListener("chunk", (event) => { const payload = JSON.parse(event.data); if (payload.audio) { const audioData = atob(payload.audio); const bytes = new Uint8Array(audioData.length); for (let i = 0; i < audioData.length; i++) { bytes[i] = audioData.charCodeAt(i); } audioChunks.push(bytes); } }); eventSource.addEventListener("complete", (event) => { console.log("Stream complete"); // Combine chunks and create blob const blob = new Blob(audioChunks, { type: "audio/wav" }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "audio.wav"; a.click(); eventSource.close(); }); eventSource.onerror = (error) => { console.error("SSE Error:", error); eventSource.close(); }; // Send the request fetch(url, { method: "POST", headers: headers, body: JSON.stringify(payload) }); ``` ## Error Responses Invalid text or audio configuration ```json theme={null} { "success": false, "error": { "type": "INVALID_REQUEST_ERROR", "message": "Invalid text or audio configuration." } } ``` Rate limit exceeded ```json theme={null} { "success": false, "error": { "type": "RATE_LIMIT_ERROR", "message": "Rate limit exceeded. Please try again later." } } ``` Unexpected error occurred ```json theme={null} { "success": false, "error": { "type": "API_ERROR", "message": "An unexpected error occurred while processing." } } ``` # Voice Cloned TTS (Realtime) Source: https://docs.gnani.ai/api/VC/vc-websocket Real-time cloned voice audio streaming via WebSocket. ## Overview Upload a reference audio clip to get a `speaker_embedding`. See [Voice Clone Embeddings](/api/VC/voice-clone-embeddings). Pass the `speaker_embedding` from Step 1 to this endpoint to stream cloned voice audio in real-time with the lowest latency. Stream cloned voice audio in real-time with the lowest latency via WebSocket. Pass the `speaker_embedding` from [Voice Clone Embeddings](/api/VC/voice-clone-embeddings) to use your cloned voice. For simpler use cases, see [Voice Cloning REST](/api/VC/vc-inference) or [Voice Cloning Streaming](/api/VC/vc-sse). ## Endpoint ``` wss://api.vachana.ai/api/v1/tts ``` ## Authentication All Realtime connections require the following headers: | Header | Required | Description | Example | | -------------- | -------- | ------------------------------- | ------------------- | | `Content-Type` | Yes | Must be `application/json` | `application/json` | | `X-API-Key-ID` | Yes | Your API key for authentication | `` | ## Request Format Send a JSON message with the following structure: ```json theme={null} { "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm" }, "speaker_embedding": { "embedding": "", "shape": [1, 768], "dtype": "torch.bfloat16" } } ``` ## Response The server uses the same WebSocket message protocol as [TTS Realtime](/api/TTS/tts-websocket). Send a JSON synthesis request; receive JSON-wrapped base64 audio chunks. ### Connection flow 1. Connect to `wss://api.vachana.ai/api/v1/tts` with `X-API-Key-ID`. 2. Send a JSON message with `text`, `model`, `speaker_embedding`, and `audio_config`. 3. Receive `start`, then `audio` chunks, then `complete`. 4. Decode each chunk's `data.audio` from base64 and append in order. ### Server messages **`start`** — synthesis began: ```json theme={null} { "type": "start", "message": "Streaming started", "request_id": "req_abc123" } ``` **Audio chunk:** ```json theme={null} { "type": "audio", "data": { "chunk_index": 1, "audio": "", "is_final": false } } ``` **`complete`** — all audio sent: ```json theme={null} { "type": "complete", "data": { "chunk_index": 12, "audio": "", "is_final": true } } ``` **`error`** — synthesis failed: ```json theme={null} { "type": "error", "error": "SYNTHESIS_FAILED", "message": "Human-readable error description" } ``` Include a complete `audio_config` (`sample_rate`, `num_channels`, `sample_width`, `encoding`, `container`) matching [VC REST](/api/VC/vc-inference) examples. ## Example Usage ```javascript JavaScript theme={null} const ws = new WebSocket("wss://api.vachana.ai/api/v1/tts", { headers: { "Content-Type": "application/json", "X-API-Key-ID": "", }, }); ws.on("open", () => { const request = { text: "नमस्ते, आप कैसे हैं?", model: "vachana-vc-v1", audio_config: { sample_rate: 44100, encoding: "linear_pcm", }, speaker_embedding: { embedding: "", shape: [1, 768], dtype: "torch.bfloat16", }, }; ws.send(JSON.stringify(request)); }); ws.on("message", (data) => { // Handle audio chunks console.log("Received audio chunk:", data); }); ws.on("error", (error) => { console.error("WebSocket error:", error); }); ws.on("close", () => { console.log("WebSocket connection closed"); }); ``` ```python Python theme={null} import websocket import json def on_message(ws, message): # Handle audio chunks print(f"Received audio chunk: {len(message)} bytes") def on_error(ws, error): print(f"Error: {error}") def on_close(ws, close_status_code, close_msg): print("WebSocket connection closed") def on_open(ws): request = { "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "encoding": "linear_pcm" }, "speaker_embedding": { "embedding": "", "shape": [1, 768], "dtype": "torch.bfloat16" } } ws.send(json.dumps(request)) ws = websocket.WebSocketApp( "wss://api.vachana.ai/api/v1/tts", header={ "Content-Type": "application/json", "X-API-Key-ID": "" }, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close ) ws.run_forever() ``` # Voice Clone Embeddings Source: https://docs.gnani.ai/api/VC/voice-clone-embeddings POST /api/v1/tts/voice-clone/embeddings Generate voice clone embeddings from an audio file. ## Voice Cloning Flow Voice cloning is a two-step process. Complete Step 1 once per voice, then reuse the embedding across any synthesis endpoint. Upload 5–30 seconds of clean reference audio to extract a `speaker_embedding`. Cache the result — you only need to generate it once per voice. Pass the `speaker_embedding` from Step 1 to your preferred synthesis endpoint: Full audio returned in a single response Receive audio progressively as it's synthesized Lowest latency — stream text in, audio out *** ## Overview Generate a `speaker_embedding` from a reference audio clip. Upload the file and receive a multi-dimensional embedding you can pass to any Voice Cloned TTS endpoint. # Introduction – Gnani Speech APIs Source: https://docs.gnani.ai/api/introduction/introduction Speech-to-text, text-to-speech, and voice cloning APIs for Indian languages — REST, streaming, and batch. Build with Gnani Speech APIs when you need transcription or synthesis in production — REST for short clips, WebSocket for live audio, and Batch for long or multi-file jobs. | Product | When to use it | | ------------------------------------------------------------- | --------------------------------------------------------- | | **This section** | Integrate STT, TTS, or voice cloning directly in your app | | [Agent Builder](/introduction) | Configure agents in the console | | [Agent Builder Platform API](/Platform/platform-introduction) | Manage agents, FAQs, and conversations over REST | ## Base URL ```text theme={null} https://api.vachana.ai ``` All paths in this documentation are relative to this base URL. ## Authentication Send your API key on every request: ```bash theme={null} X-API-Key-ID: ``` Generate a key from the [Gnani API dashboard](https://app.gnani.ai/voice). Missing or invalid keys return `401 Unauthorized`. Store keys in environment variables or a secrets manager. Do not commit them to source control. ## Available APIs ### Speech-to-Text — Gnani Prisma v2.5 | API | Best for | Guide | | ------------ | -------------------------------------------- | ---------------------------------------------- | | STT REST | Audio up to 60 seconds, single HTTP response | [Speech-to-Text REST](/api/STT/speech-to-text) | | STT Realtime | Live microphone or streaming audio | [STT Realtime](/api/STT/stt-websocket) | | STT Batch | Long files, many files, offline pipelines | [Batch STT](/api/STTBatch/Introduction) | ### Text-to-Speech — Gnani Timbre v2.5 Timbre v2.5 is the recommended model. Timbre v2.0 is deprecated and will be removed in a future release. | API | Best for | Guide | | ------------- | --------------------------------- | --------------------------------------------- | | TTS REST | Full audio in one response | [TTS REST](/api/TTS/tts-inference) | | TTS Streaming | Progressive playback via SSE | [TTS Streaming](/api/TTS/tts-sse) | | TTS Realtime | Lowest latency over WebSocket | [TTS Realtime](/api/TTS/tts-websocket) | | Voice catalog | Pick a voice before you integrate | [Available Voices](/api/TTS/available-voices) | ### Voice cloning | API | Best for | Guide | | ------------- | ------------------------------------------------- | -------------------------------------------------------- | | VC Embeddings | Create a `speaker_embedding` from reference audio | [Voice Clone Embeddings](/api/VC/voice-clone-embeddings) | | VC REST | Cloned voice in one HTTP response | [VC REST](/api/VC/vc-inference) | | VC Streaming | Cloned voice over SSE | [VC Streaming](/api/VC/vc-sse) | | VC Realtime | Cloned voice over WebSocket | [VC Realtime](/api/VC/vc-websocket) | ## Key capabilities | Capability | Details | | ------------- | -------------------------------------------------------------- | | Languages | 10+ Indian languages with native-script output | | Audio formats | WAV, MP3, OGG, FLAC, AAC, M4A (STT) | | Voice cloning | Clone from a short reference sample | | Latency | P95 \~200 ms for STT; streaming TTS supported | | Modes | Real-time, REST, and batch | | Formatting | Auto-punctuation and text normalization on supported endpoints | | SSML | Supported on TTS for fine-grained control | ## Integrations | Plugin | Guide | | ------- | --------------------------------------- | | LiveKit | [LiveKit plugin](/livekit/introduction) | | Pipecat | [Pipecat plugin](/pipecat/introduction) | ## Use case guides | Guide | Description | | ------------------------------------------------------------- | ------------------------------------- | | [Podcast transcription](/api/use-cases/podcast-transcription) | Speaker-separated batch transcription | | [Call analytics](/api/use-cases/call-analytics) | Batch STT plus LLM analysis | | [Real-time compliance](/api/use-cases/real-time-compliance) | Live WebSocket monitoring | ## Get started New to the APIs? Start here: [Quick Start](/api/introduction/quick-start) *** ## Docs feedback Docs feedback: [Gnani APIs docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571) **Last verified:** 5 August 2026 # Quick Start Source: https://docs.gnani.ai/api/introduction/quick-start Make your first speech-to-text, text-to-speech or voice-cloned TTS API request in minutes. ## Prerequisites Before you begin, ensure you have: * A valid API key (sign up on the [Gnani API platform](https://app.gnani.ai/voice/) to generate API keys) * cURL installed, or an API client such as Postman Use a test audio file that meets these requirements: * Format: WAV, MP3, OGG, FLAC, AAC, or M4A * Sample rate: 8 kHz to 44.1 kHz * Maximum duration: 60 seconds ### Your First Speech-to-Text Request Minimal example to transcribe a Hindi audio file: ```bash theme={null} curl -X POST https://api.vachana.ai/stt/v3 \ -H 'Content-Type: multipart/form-data' \ -H 'X-API-Key-ID: ' \ -F audio_file='@/path/to/your/audio.wav' \ -F language_code=hi-IN ``` **Replace these values:** * ``: Your Gnani Prisma v2.5 API key * `/path/to/your/audio.wav`: Path to your audio file * `hi-IN`: Language code (see [Language Codes](/api/STT/speech-to-text#language-codes)) ### Expected STT Response On success, you'll receive a JSON response like: ```json theme={null} { "success": true, "request_id": "019fd17d-1106-7265-88ab-3ed12d029292", "transcript": "नमस्ते, आप कैसे हैं?", "model": "gnani-prisma-v2.5" } ``` Stream live audio over WebSocket. See [STT Realtime](/api/STT/stt-websocket) for the full protocol, headers, and PCM requirements. **Endpoint** ```text theme={null} wss://api.vachana.ai/stt/v3/stream ``` **Required headers** | Header | Value | | -------------- | -------------------------- | | `x-api-key-id` | Your API key | | `lang_code` | BCP-47 code (e.g. `hi-IN`) | Send **binary PCM frames** only (16-bit mono, 8 or 16 kHz, 1024 bytes per frame). The server returns JSON transcript events: ```json theme={null} { "type": "transcript", "text": "Hello, how are you today?", "segment_id": "seg_abc123", "latency": 320 } ``` Timbre v2.5 is now the recommended TTS model. Migrate from Timbre v2.0 to continue receiving the latest improvements. Timbre v2.0 will be deprecated soon. Have your input text ready. Choose a voice from the [Voice Catalog](/api/TTS/available-voices). ### Your First Text-to-Speech Call Minimal example for REST TTS (synchronous audio). This endpoint returns the full synthesized audio as a binary response. ```bash theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/inference \ -H 'Content-Type: application/json' \ -H 'X-API-Key-ID: ' \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" } }' \ --output response.wav ``` ### Expected TTS Response A successful request will return a `200 OK` HTTP status. The response body will contain raw binary audio data representing the synthesized text, adhering to the format specified in your `audio_config`. ```http theme={null} HTTP/1.1 200 OK Content-Type: audio/wav ``` This endpoint streams synthesized audio using Server-Sent Events (SSE). Audio is generated and delivered incrementally as it becomes available. ### Your First Streaming Call ```bash theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/sse \ -H 'Content-Type: application/json' \ -H 'X-API-Key-ID: ' \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "speed": 1.0, "audio_config": { "sample_rate": 48000, "encoding": "linear_pcm", "container": "wav" } }' ``` ### Expected SSE Response A successful request will return a `200 OK` HTTP status. The response body will contain a stream of server-sent events. Each chunk contains base64 encoded audio fragments. ```http theme={null} HTTP/1.1 200 OK Content-Type: text/event-stream event: start data: {"status": "streaming_started", "text": "नमस्ते, आप कैसे हैं?"} event: chunk data: {"chunk_index": 1, "audio": "", "is_final": false} event: complete data: {"chunk_index": 2, "audio": "", "is_final": true} ``` Connect over WebSocket for lowest latency. See [TTS Realtime](/api/TTS/tts-websocket) for message types and `audio_config` options. **Endpoint** ```text theme={null} wss://api.vachana.ai/api/v1/tts ``` Send a JSON payload after connecting (include `X-API-Key-ID` in the upgrade headers): ```json theme={null} { "text": "नमस्ते, आप कैसे हैं?", "voice": "Nalini", "model": "timbre-v2.5", "language": "hi-IN", "audio_config": { "sample_rate": 48000, "encoding": "linear_pcm", "container": "wav" } } ``` The server streams JSON messages with base64 audio chunks (`start`, then `audio`, then `complete`). Voice cloning works in two steps: 1. **Generate embeddings** — upload a reference audio file to get a `speaker_embedding` 2. **Synthesize** — pass the embedding with your text to any VC TTS endpoint ### Step 1: Generate Voice Embeddings Upload a reference audio file (WAV/MP3, ideally 5–30 seconds of clear speech): ```bash theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/voice-clone/embeddings \ -H 'X-API-Key-ID: ' \ -F audio_file='@/path/to/reference.wav' ``` **Replace these values:** * ``: Your Gnani API key * `/path/to/reference.wav`: Path to your reference audio file ### Expected Embeddings Response ```json theme={null} { "embedding": "", "shape": [1, 768], "dtype": "torch.bfloat16" } ``` ### Step 2: Synthesize with Your Cloned Voice Pass the `speaker_embedding` from Step 1 to synthesize audio in your cloned voice: ```bash theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/inference \ -H 'Content-Type: application/json' \ -H 'X-API-Key-ID: ' \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "audio_config": { "sample_rate": 44100, "num_channels": 1, "sample_width": 2, "encoding": "linear_pcm", "container": "wav" }, "speaker_embedding": { "embedding": "", "shape": [1, 768], "dtype": "torch.bfloat16" } }' \ --output cloned_voice.wav ``` A successful request returns a `200 OK` with raw binary audio data in the specified format. Stream cloned voice audio progressively via Server-Sent Events: ```bash theme={null} curl -X POST https://api.vachana.ai/api/v1/tts/sse \ -H 'Content-Type: application/json' \ -H 'X-API-Key-ID: ' \ -d '{ "text": "नमस्ते, आप कैसे हैं?", "model": "vachana-vc-v1", "speaker_embedding": { "embedding": "", "shape": [1, 768], "dtype": "torch.bfloat16" } }' ``` The response streams base64-encoded audio chunks as server-sent events, identical in format to the TTS SSE response. For the lowest latency, stream text and receive cloned voice audio over a WebSocket: ```javascript theme={null} const ws = new WebSocket("wss://api.vachana.ai/api/v1/tts", { headers: { "Content-Type": "application/json", "X-API-Key-ID": "", }, }); ws.on("open", () => { ws.send(JSON.stringify({ text: "नमस्ते, आप कैसे हैं?", model: "vachana-vc-v1", audio_config: { sample_rate: 44100, encoding: "linear_pcm" }, speaker_embedding: { embedding: "", shape: [1, 768], dtype: "torch.bfloat16", }, })); }); ws.on("message", (data) => { // Handle binary PCM audio chunks }); ``` The server streams binary PCM audio chunks over the WebSocket connection. *** ## Next Steps * **Speech-to-Text**: [STT REST](/api/STT/speech-to-text) and [STT Realtime](/api/STT/stt-websocket) * **Text-to-Speech**: [REST](/api/TTS/tts-inference), [Streaming (SSE)](/api/TTS/tts-sse), and [Realtime](/api/TTS/tts-websocket) * **Voice Cloning**: [VC Embeddings](/api/VC/voice-clone-embeddings), [REST](/api/VC/vc-inference), [Streaming](/api/VC/vc-sse), and [Realtime](/api/VC/vc-websocket) * **Batch transcription**: [Batch STT Introduction](/api/STTBatch/Introduction) # Call Analytics Pipeline Source: https://docs.gnani.ai/api/use-cases/call-analytics A production-ready Python pipeline for call analytics using the Gnani Prisma v2.5 Batch STT API. Covers async transcription with two-speaker diarization, segment-level sentiment, and LLM-powered analysis via Claude or OpenAI. ## Overview Every customer call contains signal that most teams never act on. This pipeline surfaces that signal automatically — agent effectiveness, customer sentiment, resolution quality, and upsell opportunities — using the Batch STT API for transcription and an LLM for structured analysis. | Industry | What the pipeline enables | | -------------------------- | ------------------------------------------------------------------------------------------------------ | | **BFSI / Collections** | Monitor agent compliance, detect customer distress early, flag missed EMI restructuring opportunities. | | **Insurance** | Analyze claim support calls, track resolution rates, identify policy renewal signals. | | **Contact Centers / BPOs** | Automate QA at scale, reduce manual call review, improve agent training with structured feedback. | | **Healthcare** | Analyze patient support calls, surface unresolved queries, track sentiment across touchpoints. | | **Telecom** | Detect churn signals, identify upsell triggers, monitor service complaint patterns. | **Sentiment via LLM.** The Batch STT API returns speaker-separated segments with timestamps. Derive sentiment arcs, emotion shifts, and resolution quality in the LLM analysis step — no native sentiment fields are returned from the transcription layer. *** ## Prerequisites & Installation ```bash theme={null} # HTTP client for Batch STT pip install requests # Install whichever LLM provider you intend to use pip install anthropic # Claude pip install openai # OpenAI / ChatGPT ``` **ffmpeg for audio prep.** If you need to compress or convert audio before upload (10 MB per-file limit), install ffmpeg with `brew install ffmpeg` on macOS or `apt install ffmpeg` on Linux. *** ## Authentication Every request to the Batch STT API requires the `X-API-Key-ID` header. Store all credentials in environment variables. | Header | Required | Description | | ------------------ | -------- | ---------------------------------------------------------------------------------------------- | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key. Required on every Batch STT request. | | `X-API-Request-ID` | No | A UUID trace ID you assign. Used to correlate your logs with platform logs or support tickets. | ```bash .env theme={null} # Vachana GNANI_API_KEY=your-api-key # LLM provider — set whichever you will use ANTHROPIC_API_KEY=your-anthropic-key OPENAI_API_KEY=your-openai-key # Switch between "claude" and "openai" LLM_PROVIDER=claude ``` **Never hardcode API keys.** Do not commit API keys to version control. Use environment variables, a secrets manager, or a vault. Rotate keys immediately if exposed. *** ## Supported Languages | Language | Code | Script | | ------------------------ | ------- | ------------------ | | **Bengali (Bangladesh)** | `bn-BD` | Bengali | | **Bengali (India)** | `bn-IN` | Bengali | | **English (India)** | `en-IN` | Latin | | **Hindi** | `hi-IN` | Devanagari | | **Hinglish** | `hi-en` | Latin + Devanagari | | **Kannada** | `kn-IN` | Kannada | | **Malayalam** | `ml-IN` | Malayalam | | **Marathi** | `mr-IN` | Devanagari | | **Tamil** | `ta-IN` | Tamil | | **Telugu** | `te-IN` | Telugu | **ITN not supported on Batch STT.** Transcripts are returned verbatim. Use [REST STT](/api/STT/speech-to-text) if you need Inverse Text Normalization on short clips. *** ## Batch API Flow Creating a job does **not** start transcription. You must call `/start` after upload. | Step | Method | Endpoint | Reference | | ------------------ | ------ | ---------------------------------------------------- | -------------------------------------------- | | **1. Create job** | `POST` | `/stt/v3/batch/jobs` | [Create Job](/api/STTBatch/Create_Job) | | **2. Start job** | `POST` | `/stt/v3/batch/jobs/{job_id}/start` | [Start Job](/api/STTBatch/Start_Job) | | **3. Poll status** | `GET` | `/stt/v3/batch/jobs/{job_id}` | [Get Job Status](/api/STTBatch/Get_Job) | | **4. List files** | `GET` | `/stt/v3/batch/jobs/{job_id}/files?status=COMPLETED` | [Get Job Files](/api/STTBatch/Get_Job_Files) | | **5. Download** | `GET` | `` | JSON with `full_transcript` + `segments` | Upload 1–100 audio files as multipart form data with a `config` JSON field (`model`, `language_code`, diarization settings). Receive a `job_id` with status `CREATED`. Transcription has not started yet. Trigger processing. Status transitions: `STARTING` → `QUEUED` → `IN_PROGRESS` → `COMPLETED` (or a terminal failure state). Call the status endpoint every **10 seconds** until status reaches a terminal state. Job status and file counts are returned — not transcript text. Each completed file includes a `transcript_url` (valid for 1 hour). Download each URL to retrieve `full_transcript` and `segments`. Build speaker-separated conversation threads and talk-time logs from segments. Send the parsed transcript to Claude or OpenAI for structured analysis including sentiment arc. **Minimum poll interval: 10 seconds.** Do not poll more frequently than every 10 seconds for the same `job_id`. *** ## Pipeline Implementation ### Imports & Setup ```python imports and config theme={null} import os, json, time, hashlib, requests from pathlib import Path from datetime import datetime from typing import List, Dict, Optional try: import anthropic except ImportError: anthropic = None try: from openai import OpenAI except ImportError: OpenAI = None OUTPUT_DIR = "outputs" BATCH_JOBS = "https://api.vachana.ai/stt/v3/batch/jobs" BATCH_START = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start" BATCH_STATUS = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}" BATCH_FILES = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files" POLL_INTERVAL = 10 # seconds — minimum recommended interval TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"} LLM_PROVIDER = os.getenv("LLM_PROVIDER", "claude") Path(OUTPUT_DIR).mkdir(exist_ok=True) ``` ### Create, Start & Poll ```python process_audio_files + _poll_until_complete theme={null} def process_audio_files( self, audio_paths: List[str], language_code: str = "hi-IN", ) -> Dict[str, dict]: """Create a Batch STT job, start it, poll until complete, and download transcripts.""" if not audio_paths: return {} config = { "model": "gnani-prisma-v2.5", "language_code": language_code, "mode": "transcribe", "with_diarization": True, "num_speakers": 2, "is_multi_channel": False, } files = [("config", (None, json.dumps(config), "application/json"))] for path in audio_paths: files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav"))) resp = requests.post(BATCH_JOBS, headers=self.headers, files=files) for key, (_, fh, _) in files: if key == "files": fh.close() resp.raise_for_status() job_id = resp.json()["job_id"] print(f"Job created: {job_id}") start_resp = requests.post(BATCH_START.format(job_id=job_id), headers=self.headers) start_resp.raise_for_status() print(f"Job started. status: {start_resp.json()['status']}") if not self._poll_until_complete(job_id): return {} output_dir = Path(OUTPUT_DIR) / f"job_{job_id}" output_dir.mkdir(parents=True, exist_ok=True) transcriptions = self._download_and_parse(job_id, output_dir) self.transcriptions.update(transcriptions) print(f"Transcribed {len(transcriptions)} file(s).") for fname, d in transcriptions.items(): self.analyze_transcription(d["conversation_path"], output_dir, fname) return transcriptions def _poll_until_complete(self, job_id: str) -> bool: """Poll the job status endpoint every 10 s until a terminal state is reached.""" url = BATCH_STATUS.format(job_id=job_id) print("Polling for completion (every 10 s)...") while True: time.sleep(POLL_INTERVAL) r = requests.get(url, headers=self.headers) r.raise_for_status() payload = r.json() status = payload["status"] print(f" status={status} {payload.get('completed_files', 0)}/{payload.get('total_files', '?')} files") if status == "COMPLETED": return True if status in TERMINAL_STATUSES: print(f"Job ended with status: {status}") return False ``` ### Download & Parse — Speaker Transcripts & Talk Time `_download_and_parse` fetches completed file entries, downloads each `transcript_url`, and writes two output files per call: a speaker-labelled conversation transcript and a per-speaker talk-time log. ```python _download_and_parse theme={null} def _download_and_parse(self, job_id: str, output_dir: Path) -> Dict[str, dict]: """Download transcript URLs and parse per-file segment data.""" transcriptions = {} resp = requests.get( BATCH_FILES.format(job_id=job_id), headers=self.headers, params={"status": "COMPLETED"}, ) resp.raise_for_status() file_entries = resp.json().get("files", []) for entry in file_entries: transcript_url = entry.get("transcript_url") if not transcript_url: continue transcript = requests.get(transcript_url).json() fname = Path(entry["original_path"]).stem segments = transcript.get("segments", []) if not segments: print(f"No segments returned for {fname}, skipping.") continue lines, speaker_times = [], {} for seg in segments: spk = seg.get("speaker_id", "UNKNOWN") text = seg.get("text", "").strip() s = seg.get("start_time", 0.0) e = seg.get("end_time", 0.0) lines.append(f"SPEAKER_{spk}: {text}") speaker_times[spk] = speaker_times.get(spk, 0.0) + (e - s) conv_path = output_dir / f"{fname}_conversation.txt" timing_path = output_dir / f"{fname}_timing.json" conv_path.write_text("\n".join(lines), encoding="utf-8") timing_path.write_text(json.dumps(speaker_times, indent=2), encoding="utf-8") transcriptions[fname] = { "conversation_path": str(conv_path), "timing_path": str(timing_path), } return transcriptions ``` **Files produced per call:** `{name}_conversation.txt` — speaker-labelled transcript · `{name}_timing.json` — talk time per speaker in seconds ### LLM Analysis The analysis step sends the parsed conversation to your chosen LLM with a structured prompt. Sentiment arc, resolution quality, and upsell signals are inferred here — not from the STT response. Switch providers by changing the `LLM_PROVIDER` environment variable. ```python analysis prompt theme={null} ANALYSIS_PROMPT = """ Analyze this call transcription from start to finish. TRANSCRIPTION: {transcription} Provide a structured response covering each of the following: 1. Speaker identification — which speaker is the customer, which is the agent? 2. Customer type — new/potential customer or existing customer? 3. Opening problem — what issue or query did the customer raise initially? 4. Products or services — what was the customer inquiring about or facing issues with? 5. Agent response — how did the agent handle and resolve the issue throughout the call? 6. Resolution outcome — was the issue resolved? Was the customer satisfied at the end? 7. Sentiment arc — how did the customer's sentiment shift across the call? Infer this from the conversation content. 8. Upsell or cross-sell signals — any opportunities the agent identified or missed? 9. Competitor mentions — were any competitors referenced? 10. Summary — two-sentence outcome summary. """ ``` ```python analyze_transcription + _call_llm theme={null} def analyze_transcription(self, conversation_path: str, output_dir: Path, file_name: str) -> dict: """Run LLM analysis on a parsed conversation file.""" transcript = Path(conversation_path).read_text(encoding="utf-8") analysis = self._call_llm( system="You are a call analytics expert. Provide structured, actionable insights.", user=ANALYSIS_PROMPT.format(transcription=transcript), ) out = output_dir / f"{file_name}_analysis.txt" out.write_text(analysis.strip(), encoding="utf-8") print(f"Analysis saved: {out}") return {"file_name": file_name, "analysis_path": str(out)} def _call_llm(self, system: str, user: str) -> str: """Route to Claude or OpenAI based on LLM_PROVIDER env variable.""" if LLM_PROVIDER == "claude": if anthropic is None: raise ImportError("Install the anthropic package: pip install anthropic") client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) msg = client.messages.create( model="claude-opus-4-8", max_tokens=2000, system=system, messages=[{"role": "user", "content": user}], ) return msg.content[0].text elif LLM_PROVIDER == "openai": if OpenAI is None: raise ImportError("Install the openai package: pip install openai") client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) resp = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system}, {"role": "user", "content": user}, ], ) return resp.choices[0].message.content raise ValueError(f"Unknown LLM_PROVIDER '{LLM_PROVIDER}'. Set to 'claude' or 'openai'.") ``` ### Ad-hoc Q\&A Ask any question against a transcribed call — useful for targeted investigation after bulk processing. ```python answer_question theme={null} def answer_question(self, question: str) -> None: """Answer a question for every transcribed call in the current session.""" for fname, data in self.transcriptions.items(): transcript = Path(data["conversation_path"]).read_text(encoding="utf-8") answer = self._call_llm( system="", user=f"TRANSCRIPT:\n{transcript}\n\nQUESTION: {question}", ) q_hash = hashlib.sha1(question.encode()).hexdigest()[:6] out = Path(data["conversation_path"]).parent / f"{fname}_q_{q_hash}.txt" out.write_text(f"Q: {question}\n\nA:\n{answer}", encoding="utf-8") print(f"Answer saved: {out}") ``` ### Summary Report Generate a single summary report across all analyzed calls in the session. ```python get_summary theme={null} SUMMARY_PROMPT = """ Based on this call analysis, provide a concise 2–3 word answer for each point: {analysis_text} 1. Customer and Agent 2. Customer Type 3. Main Issue 4. Service Discussed 5. Agent Response Quality 6. Customer Satisfaction 7. Overall Sentiment 8. Competitor or Upsell Signal 9. Resolution Status """ def get_summary(self) -> None: """Generate a single summary report across all calls in the session.""" ts = datetime.now().strftime("%Y%m%d_%H%M%S") out = Path(OUTPUT_DIR) / f"summary_{ts}.txt" with open(out, "w", encoding="utf-8") as f: f.write(f"CALL ANALYTICS SUMMARY\n{'='*60}\n") f.write(f"Generated : {datetime.now()}\n") f.write(f"Total calls: {len(self.transcriptions)}\n{'='*60}\n\n") for fname, data in self.transcriptions.items(): af = Path(data["conversation_path"]).parent / f"{fname}_analysis.txt" if not af.exists(): print(f"No analysis file found for {fname}, skipping.") continue summary = self._call_llm( system="You are a call analytics expert. Be concise.", user=SUMMARY_PROMPT.format(analysis_text=af.read_text(encoding="utf-8")), ) f.write(f"Call: {fname}\n{'-'*30}\n{summary.strip()}\n\n") print(f"Summary saved: {out}") ``` *** ## Full Pipeline ```python call_analytics_pipeline.py theme={null} import os, json, time, hashlib, requests from pathlib import Path from datetime import datetime from typing import List, Dict, Optional try: import anthropic except ImportError: anthropic = None try: from openai import OpenAI except ImportError: OpenAI = None OUTPUT_DIR = "outputs" BATCH_JOBS = "https://api.vachana.ai/stt/v3/batch/jobs" BATCH_START = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start" BATCH_STATUS = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}" BATCH_FILES = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files" POLL_INTERVAL = 10 LLM_PROVIDER = os.getenv("LLM_PROVIDER", "claude") TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"} Path(OUTPUT_DIR).mkdir(exist_ok=True) ANALYSIS_PROMPT = """ Analyze this call transcription from start to finish. TRANSCRIPTION: {transcription} Provide a structured response covering each of the following: 1. Speaker identification — which speaker is the customer, which is the agent? 2. Customer type — new/potential customer or existing customer? 3. Opening problem — what issue or query did the customer raise initially? 4. Products or services — what was the customer inquiring about or facing issues with? 5. Agent response — how did the agent handle and resolve the issue throughout the call? 6. Resolution outcome — was the issue resolved? Was the customer satisfied at the end? 7. Sentiment arc — how did the customer's sentiment shift across the call? Infer this from the conversation content. 8. Upsell or cross-sell signals — any opportunities the agent identified or missed? 9. Competitor mentions — were any competitors referenced? 10. Summary — two-sentence outcome summary. """ SUMMARY_PROMPT = """ Based on this call analysis, provide a concise 2–3 word answer for each point: {analysis_text} 1. Customer and Agent 2. Customer Type 3. Main Issue 4. Service Discussed 5. Agent Response Quality 6. Customer Satisfaction 7. Overall Sentiment 8. Competitor or Upsell Signal 9. Resolution Status """ class CallAnalytics: def __init__(self, api_key: str): self.headers = {"X-API-Key-ID": api_key} self.transcriptions: Dict[str, dict] = {} def process_audio_files(self, audio_paths: List[str], language_code: str = "hi-IN") -> Dict[str, dict]: if not audio_paths: return {} config = { "model": "gnani-prisma-v2.5", "language_code": language_code, "mode": "transcribe", "with_diarization": True, "num_speakers": 2, "is_multi_channel": False, } files = [("config", (None, json.dumps(config), "application/json"))] for path in audio_paths: files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav"))) resp = requests.post(BATCH_JOBS, headers=self.headers, files=files) for key, (_, fh, _) in files: if key == "files": fh.close() resp.raise_for_status() job_id = resp.json()["job_id"] print(f"Job created: {job_id}") start_resp = requests.post(BATCH_START.format(job_id=job_id), headers=self.headers) start_resp.raise_for_status() print(f"Job started. status: {start_resp.json()['status']}") if not self._poll_until_complete(job_id): return {} output_dir = Path(OUTPUT_DIR) / f"job_{job_id}" output_dir.mkdir(parents=True, exist_ok=True) transcriptions = self._download_and_parse(job_id, output_dir) self.transcriptions.update(transcriptions) print(f"Transcribed {len(transcriptions)} file(s).") for fname, d in transcriptions.items(): self.analyze_transcription(d["conversation_path"], output_dir, fname) return transcriptions def _poll_until_complete(self, job_id: str) -> bool: url = BATCH_STATUS.format(job_id=job_id) print("Polling every 10 s...") while True: time.sleep(POLL_INTERVAL) r = requests.get(url, headers=self.headers) r.raise_for_status() p = r.json() print(f" [{p['status']}] {p.get('completed_files', 0)}/{p.get('total_files', '?')} files") if p["status"] == "COMPLETED": return True if p["status"] in TERMINAL_STATUSES: print(f"Job ended: {p['status']}") return False def _download_and_parse(self, job_id: str, output_dir: Path) -> Dict[str, dict]: transcriptions = {} resp = requests.get( BATCH_FILES.format(job_id=job_id), headers=self.headers, params={"status": "COMPLETED"}, ) resp.raise_for_status() for entry in resp.json().get("files", []): transcript_url = entry.get("transcript_url") if not transcript_url: continue transcript = requests.get(transcript_url).json() fname = Path(entry["original_path"]).stem segments = transcript.get("segments", []) if not segments: continue lines, speaker_times = [], {} for seg in segments: spk = seg.get("speaker_id", "UNKNOWN") txt = seg.get("text", "").strip() s, e = seg.get("start_time", 0.0), seg.get("end_time", 0.0) lines.append(f"SPEAKER_{spk}: {txt}") speaker_times[spk] = speaker_times.get(spk, 0.0) + (e - s) cp = output_dir / f"{fname}_conversation.txt" tp = output_dir / f"{fname}_timing.json" cp.write_text("\n".join(lines), encoding="utf-8") tp.write_text(json.dumps(speaker_times, indent=2), encoding="utf-8") transcriptions[fname] = {"conversation_path": str(cp), "timing_path": str(tp)} return transcriptions def _call_llm(self, system: str, user: str) -> str: if LLM_PROVIDER == "claude": client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) msg = client.messages.create(model="claude-opus-4-8", max_tokens=2000, system=system, messages=[{"role": "user", "content": user}]) return msg.content[0].text elif LLM_PROVIDER == "openai": client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) resp = client.chat.completions.create(model="gpt-4o", messages=[{"role": "system", "content": system}, {"role": "user", "content": user}]) return resp.choices[0].message.content raise ValueError(f"Unknown LLM_PROVIDER: {LLM_PROVIDER}") def analyze_transcription(self, conversation_path: str, output_dir: Path, fname: str): transcript = Path(conversation_path).read_text(encoding="utf-8") analysis = self._call_llm(system="You are a call analytics expert. Provide structured, actionable insights.", user=ANALYSIS_PROMPT.format(transcription=transcript)) out = output_dir / f"{fname}_analysis.txt" out.write_text(analysis.strip(), encoding="utf-8") print(f"Analysis: {out}") def answer_question(self, question: str): for fname, data in self.transcriptions.items(): transcript = Path(data["conversation_path"]).read_text(encoding="utf-8") answer = self._call_llm(system="", user=f"TRANSCRIPT:\n{transcript}\n\nQUESTION: {question}") q_hash = hashlib.sha1(question.encode()).hexdigest()[:6] out = Path(data["conversation_path"]).parent / f"{fname}_q_{q_hash}.txt" out.write_text(f"Q: {question}\n\nA:\n{answer}", encoding="utf-8") print(f"Q&A: {out}") def get_summary(self): ts = datetime.now().strftime("%Y%m%d_%H%M%S") out = Path(OUTPUT_DIR) / f"summary_{ts}.txt" with open(out, "w", encoding="utf-8") as f: f.write(f"CALL ANALYTICS SUMMARY\n{'='*60}\nGenerated: {datetime.now()}\n{'='*60}\n\n") for fname, data in self.transcriptions.items(): af = Path(data["conversation_path"]).parent / f"{fname}_analysis.txt" if not af.exists(): continue summary = self._call_llm(system="Be concise.", user=SUMMARY_PROMPT.format(analysis_text=af.read_text(encoding="utf-8"))) f.write(f"Call: {fname}\n{'-'*30}\n{summary.strip()}\n\n") print(f"Summary: {out}") if __name__ == "__main__": analytics = CallAnalytics(api_key=os.getenv("GNANI_API_KEY")) analytics.process_audio_files( audio_paths=["./call_001.wav"], language_code="hi-IN", ) analytics.answer_question("Did the agent offer any EMI or payment extension options?") analytics.get_summary() ``` *** ## Sample Output ```text theme={null} outputs/ └── job_019fa79e-81f9-7a8a-a446-6eff16ddec30/ ├── call_001_conversation.txt ← speaker-labelled transcript ├── call_001_timing.json ← talk time per speaker (seconds) ├── call_001_analysis.txt ← LLM structured analysis (includes sentiment arc) └── call_001_q_a3f9b2.txt ← ad-hoc Q&A answer summary_20251226_143052.txt ← batch summary across all calls ``` **call\_001\_conversation.txt** ```text theme={null} SPEAKER_1: नमस्ते, मेरा नाम रोहन है। मेरी EMI अगले हफ्ते due है। SPEAKER_2: नमस्ते रोहन जी, आपका loan account number बताइए। SPEAKER_1: हाँ, 45000 rupees की EMI है। क्या मुझे extension मिल सकता है? SPEAKER_2: आपकी request process करते हैं। 3 दिन का extension approve हो सकता है। SPEAKER_1: ठीक है, शुक्रिया। ``` **call\_001\_analysis.txt (excerpt)** ```text theme={null} 1. Speaker Identification SPEAKER_1 — Customer (Rohan) SPEAKER_2 — Agent 2. Customer Type Existing customer with an active loan account and an upcoming EMI. 3. Opening Problem Customer called to request an EMI payment extension due to cash flow constraints. 6. Resolution Outcome Resolved within the call. Customer expressed satisfaction before closing. 7. Sentiment Arc Started neutral-to-anxious. Shifted to relieved after the extension was confirmed. 8. Upsell / Cross-sell Signals No signals identified or pursued. Loan restructuring or a credit health check could have been offered — it was not. 10. Summary The customer's EMI extension request was resolved within a single call. Agent resolution quality was high; a potential upsell moment was missed. ``` *** ## Limits & Notes | Constraint | Value | What to do | | --------------------------- | ---------------------- | ---------------------------------------------------------------------------------------- | | **Max file size** | 10 MB per file | Compress or re-encode before upload. | | **Max files per job** | 100 files | Group calls into jobs of up to 100 and submit sequentially for larger batches. | | **Poll interval** | 10 seconds minimum | Do not poll more frequently than every 10 s per `job_id`. | | **Speaker diarization** | Max 2 speakers | Designed for two-party calls (agent + customer). Set `num_speakers: 2`. | | **`transcript_url` expiry** | 1 hour | Re-call [Get Job Files](/api/STTBatch/Get_Job_Files) if a URL expires. | | **ITN** | Not supported on Batch | Transcripts are verbatim. | | **LLM token limits** | Varies by provider | For calls over 30 minutes, chunk the transcript before sending to the LLM analysis step. | **Related docs:** [Batch STT Introduction](/api/STTBatch/Introduction) · [Create Job](/api/STTBatch/Create_Job) · [Start Job](/api/STTBatch/Start_Job) · [Get Job Status](/api/STTBatch/Get_Job) · [Get Job Files](/api/STTBatch/Get_Job_Files) · [REST STT for short clips](/api/STT/speech-to-text) # Podcast Transcription with Speaker Labels Source: https://docs.gnani.ai/api/use-cases/podcast-transcription Transcribe multi-speaker audio at scale using the Gnani Prisma v2.5 Batch STT API. From submitting an audio file to receiving a clean, speaker-separated transcript — in 10 Indian languages. ## Overview Transcribe multi-speaker audio at scale using the Gnani Prisma v2.5 Batch STT API. This guide walks through every step — from submitting an audio file to receiving a clean, speaker-separated transcript — using podcast transcription as the working example. Audio-first content — podcasts, interview recordings, panel discussions — carries information that stays locked unless it is transcribed. Speaker-level transcription is what separates a readable document from a wall of undifferentiated text. You know who said what, when they said it, and for how long. | Capability | What it enables downstream | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Speaker-separated output** | Per-speaker text blocks mean editors can review one voice at a time, and content teams can attribute quotes accurately. | | **Time-aligned segments** | Every segment carries a `start_time` and `end_time`, enabling subtitle generation, chapter markers, and clip extraction at a specific timestamp. | | **9 Indian languages** | Transcribe Hindi, Tamil, Telugu, Kannada, Malayalam, Bengali (India and Bangladesh), Marathi, and English without switching providers or pipelines. | | **Batch processing** | Submit up to 100 files in a single job. Run overnight jobs, backfill archives, or process weekly episode batches without managing queues yourself. | *** ## Other Use Cases The same create-start-poll-download pipeline works for any long-form, speaker-rich audio. Any scenario involving long audio files, two speakers, and a need for speaker-separated text maps directly to this pipeline. | Use Case | Description | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Journalist Interviews** | Transcribe field recordings with interviewer and subject separated. Feed directly into editorial workflows without manual formatting. | | **Parliamentary & Panel Debates** | Attribute statements to the correct speaker for political reporting, fact-checking, or archival. Supports Devanagari and regional scripts natively. | | **EdTech Lecture Recordings** | Transcribe faculty and student exchanges. Generate accessible transcripts for students, search indexes for course platforms, and study material exports. | | **Legal Depositions & Hearings** | Produce verbatim speaker-attributed records of proceedings for court reporter review and archival. | | **Radio Archive Digitisation** | Backfill years of archived broadcasts into searchable, attributed text. Batch processing handles large volumes without manual queuing. | | **Corporate Town Halls & Earnings Calls** | Generate attributed transcripts of leadership Q\&A sessions. Surface speaker-specific statements for internal comms or investor relations. | | **Documentary & Film Production** | Auto-generate interview transcripts for rough-cut editing. Export time-coded speaker lines directly to editing software. | | **Doctor-Patient Consultations** | Transcribe recorded consultations with doctor and patient separated. Enable structured documentation workflows for EMR systems. | **Two-speaker limit:** The Gnani Prisma v2.5 Batch STT API supports a maximum of two distinct speakers per file. It is optimised for two-party audio — interviews, conversations, and one-on-one recordings. Panel discussions with three or more speakers are outside the current scope. *** ## Prerequisites | Requirement | Details | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Gnani Prisma v2.5 API key** | Available from the Gnani API dashboard. You will use this as the `X-API-Key-ID` header on every request. | | **Python 3.10+** | The pipeline uses f-strings, `pathlib`, and `typing` patterns that require Python 3.10 or later. | | **Audio files** | Supported formats: WAV, MP3, MP4, FLAC, OGG, Opus, M4A, AAC, WebM, AMR. Each file must be **10 MB or smaller**. Up to **100 files** per job. | ```bash theme={null} # HTTP client (used for create, start, poll, and download calls) pip install requests ``` No SDK is required for this pipeline. All calls use the standard HTTP REST endpoints. *** ## Authentication | Header | Required | Description | | ------------------ | -------- | -------------------------------------------------------------------------------------------------- | | `X-API-Key-ID` | Yes | Your Gnani Prisma v2.5 API key. Required on every Batch STT request. | | `X-API-Request-ID` | No | A UUID you assign for tracing. Useful for correlating your application logs with platform support. | Store your API key as an environment variable. Never hardcode it in source files or commit it to version control. ```bash .env theme={null} GNANI_API_KEY=your-api-key-here ``` ```python loading credentials theme={null} import os API_KEY = os.getenv("GNANI_API_KEY") HEADERS = {"X-API-Key-ID": API_KEY} ``` **Never hardcode API keys.** Do not commit credentials to version control. Use environment variables, a secrets manager, or a vault. Rotate your key immediately if it is exposed. *** ## Limits & Supported Formats | Item | Limit | | ------------------------- | ---------------------------------------------------------------------------- | | Max file size | **10 MB** per file | | Max files per job | **100** files | | Max ZIP compressed size | **50 MB** | | Max ZIP decompressed size | **200 MB** | | Minimum poll interval | **10 seconds** between status calls for the same `job_id` | | Speaker diarization | Maximum **2** speakers per file | | `transcript_url` expiry | **1 hour** — re-call [Get Job Files](/api/STTBatch/Get_Job_Files) if expired | See [Batch STT Introduction — Limits](/api/STTBatch/Introduction#limits) for the full reference. ### Supported Audio Formats `.wav` · `.mp3` · `.mp4` · `.flac` · `.ogg` · `.opus` · `.m4a` · `.aac` · `.webm` · `.amr` | Format | Extension | Notes | | -------------- | --------------- | -------------------------------------------------------------------------- | | **WAV** | `.wav` | Uncompressed. Highest quality but largest file size. | | **FLAC** | `.flac` | Lossless compression. Good balance of quality and size for archival audio. | | **AAC / M4A** | `.m4a`, `.aac` | Common podcast export format. Well-supported across recording tools. | | **MP3** | `.mp3` | Widely used lossy format. | | **OGG / Opus** | `.ogg`, `.opus` | Efficient compression — recommended for high-volume batch jobs. | | **WebM / AMR** | `.webm`, `.amr` | Supported for mobile and web-captured audio. | **Files over 10 MB:** Compress or re-encode before upload, or split into smaller segments and stitch transcripts in order after download. The Batch API does not accept individual files larger than 10 MB. *** ## Supported Languages Pass the BCP-47 code in the `language_code` field of your job `config`. | Language | Code | Native Script | | ------------------------ | ------- | ------------- | | **Bengali (Bangladesh)** | `bn-BD` | বাংলা | | **Bengali (India)** | `bn-IN` | বাংলা | | **English (India)** | `en-IN` | Latin | | **Hindi** | `hi-IN` | हिन्दी | | **Kannada** | `kn-IN` | ಕನ್ನಡ | | **Malayalam** | `ml-IN` | മലയാളം | | **Marathi** | `mr-IN` | मराठी | | **Tamil** | `ta-IN` | தமிழ் | | **Telugu** | `te-IN` | తెలుగు | **ITN not supported on Batch STT.** Inverse Text Normalization (spoken numbers, currency, dates) is not available in the Batch Jobs API. Use [REST STT](/api/STT/speech-to-text) if you need ITN on short clips. *** ## Pipeline Creating a job does **not** start transcription. You must call `/start` after upload. | Step | Endpoint | Reference | | ------------- | -------------------------------------------------------- | -------------------------------------------- | | 1. Create | `POST /stt/v3/batch/jobs` | [Create Job](/api/STTBatch/Create_Job) | | 2. Start | `POST /stt/v3/batch/jobs/{job_id}/start` | [Start Job](/api/STTBatch/Start_Job) | | 3. Poll | `GET /stt/v3/batch/jobs/{job_id}` | [Get Job Status](/api/STTBatch/Get_Job) | | 4. List files | `GET /stt/v3/batch/jobs/{job_id}/files?status=COMPLETED` | [Get Job Files](/api/STTBatch/Get_Job_Files) | | 5. Download | `GET ` | JSON with `full_transcript` + `segments` | POST to `/stt/v3/batch/jobs` with a multipart body: a `config` JSON field and one or more `files`. Receive a `job_id` with status `CREATED`. Transcription has not started yet. POST to `/stt/v3/batch/jobs/{job_id}/start`. Status moves to `STARTING`, then `QUEUED` → `IN_PROGRESS` → `COMPLETED`. GET `/stt/v3/batch/jobs/{job_id}` every **10 seconds** until status reaches a terminal state (`COMPLETED`, `PARTIAL_FAILURE`, `FAILED`, or `CANCELLED`). GET `/stt/v3/batch/jobs/{job_id}/files?status=COMPLETED`. Each completed file includes a `transcript_url` (valid for 1 hour). GET each `transcript_url` to retrieve JSON with `full_transcript` and `segments`. Group by `speaker_id`, build per-speaker text blocks with timestamps, and save output files. ### Step 1 — Create Job ```python create_job() theme={null} import json import os import requests from pathlib import Path BATCH_JOBS = "https://api.vachana.ai/stt/v3/batch/jobs" def create_job( audio_paths: list[str], language_code: str = "hi-IN", with_diarization: bool = True, num_speakers: int = 2, ) -> str: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} config = { "model": "gnani-prisma-v2.5", "language_code": language_code, "mode": "transcribe", "with_diarization": with_diarization, "is_multi_channel": False, } if with_diarization: config["num_speakers"] = num_speakers files = [ ("config", (None, json.dumps(config), "application/json")), ] for path in audio_paths: files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav"))) resp = requests.post(BATCH_JOBS, headers=headers, files=files) for key, (_, fh, _) in files: if key == "files": fh.close() resp.raise_for_status() job_id = resp.json()["job_id"] print(f"Created job. job_id: {job_id}") return job_id ``` **Multiple files:** Add additional `("files", ...)` tuples. Up to **100** files per job, each **10 MB** or smaller. ### Step 2 — Start Job ```python start_job() theme={null} BATCH_START = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start" def start_job(job_id: str) -> None: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} url = BATCH_START.format(job_id=job_id) resp = requests.post(url, headers=headers) resp.raise_for_status() print(f"Start accepted. status: {resp.json()['status']}") ``` ### Step 3 — Poll ```python poll_until_complete() theme={null} import time from typing import Optional BATCH_STATUS = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}" POLL_INTERVAL = 10 # seconds — minimum recommended interval TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"} def poll_until_complete(job_id: str) -> Optional[str]: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} url = BATCH_STATUS.format(job_id=job_id) print(f"Polling job {job_id} every {POLL_INTERVAL}s...") while True: time.sleep(POLL_INTERVAL) resp = requests.get(url, headers=headers) resp.raise_for_status() payload = resp.json() status = payload["status"] completed = payload.get("completed_files", 0) total = payload.get("total_files", "?") print(f" [{status}] {completed}/{total} files complete") if status in TERMINAL_STATUSES: if status == "COMPLETED": print(f"Job complete. {completed} file(s) transcribed.") return status print(f"Job ended with status: {status}") return status ``` **Minimum poll interval: 10 seconds.** Do not poll more frequently than every 10 seconds for the same `job_id`. ### Step 4 — Fetch Transcript URLs ```python fetch_completed_files() theme={null} BATCH_FILES = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files" def fetch_completed_files(job_id: str) -> list[dict]: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} url = BATCH_FILES.format(job_id=job_id) resp = requests.get(url, headers=headers, params={"status": "COMPLETED"}) resp.raise_for_status() return resp.json().get("files", []) ``` ### Step 5 — Download & Parse ```python download_and_parse() theme={null} import json from pathlib import Path from typing import Dict def download_transcript(transcript_url: str) -> dict: resp = requests.get(transcript_url) resp.raise_for_status() return resp.json() def parse_transcript(transcript: dict, original_path: str, output_dir: Path) -> dict: fname = Path(original_path).stem segments = transcript.get("segments", []) if not segments: print(f"Skipping {fname}: no segments") return {} lines, speaker_times, segment_meta = [], {}, [] for seg in segments: spk = seg.get("speaker_id", "UNKNOWN") text = seg.get("text", "").strip() start = seg.get("start_time", 0.0) end = seg.get("end_time", 0.0) ts = f"{int(start // 60):02d}:{int(start % 60):02d}" lines.append(f"[{ts}] SPEAKER_{spk}: {text}") speaker_times[spk] = speaker_times.get(spk, 0.0) + (end - start) segment_meta.append({ "segment_id": seg.get("segment_id"), "speaker_id": spk, "start_time": start, "end_time": end, "text": text, "language_detected": seg.get("language_detected"), }) transcript_path = output_dir / f"{fname}_transcript.txt" transcript_path.write_text("\n".join(lines), encoding="utf-8") metadata_path = output_dir / f"{fname}_metadata.json" metadata_path.write_text(json.dumps({ "filename": original_path, "full_transcript": transcript.get("full_transcript"), "duration_seconds": transcript.get("duration_seconds"), "speaker_talk_time": {f"SPEAKER_{k}": round(v, 2) for k, v in speaker_times.items()}, "segments": segment_meta, }, indent=2, ensure_ascii=False), encoding="utf-8") print(f"Parsed: {fname} → {len(lines)} segments, {len(speaker_times)} speaker(s)") return { "transcript_path": str(transcript_path), "metadata_path": str(metadata_path), } ``` *** ## Full Script ```python podcast_transcription.py theme={null} import json import os import time import requests from pathlib import Path from typing import Dict, List, Optional BATCH_JOBS = "https://api.vachana.ai/stt/v3/batch/jobs" BATCH_START = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/start" BATCH_STATUS = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}" BATCH_FILES = "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/files" POLL_INTERVAL = 10 OUTPUT_DIR = "outputs" TERMINAL_STATUSES = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"} Path(OUTPUT_DIR).mkdir(exist_ok=True) def create_job( audio_paths: List[str], language_code: str = "hi-IN", with_diarization: bool = True, num_speakers: int = 2, ) -> str: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} config = { "model": "gnani-prisma-v2.5", "language_code": language_code, "mode": "transcribe", "with_diarization": with_diarization, "is_multi_channel": False, } if with_diarization: config["num_speakers"] = num_speakers files = [("config", (None, json.dumps(config), "application/json"))] for path in audio_paths: files.append(("files", (Path(path).name, open(path, "rb"), "audio/wav"))) resp = requests.post(BATCH_JOBS, headers=headers, files=files) for key, (_, fh, _) in files: if key == "files": fh.close() resp.raise_for_status() job_id = resp.json()["job_id"] print(f"Created {len(audio_paths)} file(s). job_id: {job_id}") return job_id def start_job(job_id: str) -> None: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} resp = requests.post(BATCH_START.format(job_id=job_id), headers=headers) resp.raise_for_status() print(f"Start accepted. status: {resp.json()['status']}") def poll_until_complete(job_id: str) -> Optional[str]: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} url = BATCH_STATUS.format(job_id=job_id) print(f"Polling every {POLL_INTERVAL}s...") while True: time.sleep(POLL_INTERVAL) resp = requests.get(url, headers=headers) resp.raise_for_status() payload = resp.json() status = payload["status"] print(f" [{status}] {payload.get('completed_files', 0)}/{payload.get('total_files', '?')} files") if status in TERMINAL_STATUSES: return status if status == "COMPLETED" else None def fetch_completed_files(job_id: str) -> list: api_key = os.getenv("GNANI_API_KEY") headers = {"X-API-Key-ID": api_key} resp = requests.get( BATCH_FILES.format(job_id=job_id), headers=headers, params={"status": "COMPLETED"}, ) resp.raise_for_status() return resp.json().get("files", []) def parse_transcript(transcript: dict, original_path: str, output_dir: Path) -> Dict[str, str]: fname = Path(original_path).stem segments = transcript.get("segments", []) if not segments: return {} lines, speaker_times, segment_meta = [], {}, [] for seg in segments: spk = seg.get("speaker_id", "UNKNOWN") text = seg.get("text", "").strip() start = seg.get("start_time", 0.0) end = seg.get("end_time", 0.0) ts = f"{int(start // 60):02d}:{int(start % 60):02d}" lines.append(f"[{ts}] SPEAKER_{spk}: {text}") speaker_times[spk] = speaker_times.get(spk, 0.0) + (end - start) segment_meta.append({ "segment_id": seg.get("segment_id"), "speaker_id": spk, "start_time": start, "end_time": end, "text": text, }) transcript_path = output_dir / f"{fname}_transcript.txt" transcript_path.write_text("\n".join(lines), encoding="utf-8") metadata_path = output_dir / f"{fname}_metadata.json" metadata_path.write_text(json.dumps({ "filename": original_path, "full_transcript": transcript.get("full_transcript"), "duration_seconds": transcript.get("duration_seconds"), "speaker_talk_time": {f"SPEAKER_{k}": round(v, 2) for k, v in speaker_times.items()}, "segments": segment_meta, }, indent=2, ensure_ascii=False), encoding="utf-8") print(f"Saved: {transcript_path.name}") return {"transcript_path": str(transcript_path), "metadata_path": str(metadata_path)} if __name__ == "__main__": job_id = create_job( audio_paths=["/path/to/episode_01.wav"], language_code="hi-IN", with_diarization=True, num_speakers=2, ) start_job(job_id) if poll_until_complete(job_id): output_dir = Path(OUTPUT_DIR) / f"job_{job_id}" output_dir.mkdir(parents=True, exist_ok=True) outputs = {} for file_entry in fetch_completed_files(job_id): transcript_url = file_entry.get("transcript_url") if not transcript_url: continue transcript = requests.get(transcript_url).json() result = parse_transcript(transcript, file_entry["original_path"], output_dir) if result: outputs[Path(file_entry["original_path"]).stem] = result print(f"\nDone. {len(outputs)} transcript(s) saved to {output_dir}/") ``` *** ## Sample Output ```text theme={null} outputs/ └── job_019fa79e-81f9-7a8a-a446-6eff16ddec30/ ├── episode_01_transcript.txt ← speaker-labelled, time-stamped transcript └── episode_01_metadata.json ← full_transcript, talk time, segment detail ``` **episode\_01\_transcript.txt** ```text theme={null} [00:00] SPEAKER_1: नमस्ते, मैं हूँ रवि शर्मा और आज हम बात करेंगे भारत के स्टार्टअप इकोसिस्टम के बारे में। [00:07] SPEAKER_2: हाँ रवि जी, बहुत अच्छा विषय है। पिछले पाँच साल में बहुत कुछ बदला है। [00:14] SPEAKER_1: बिल्कुल। दो लाख करोड़ से ज़्यादा की फंडिंग आई है 2024 में। [00:22] SPEAKER_2: और यूनिकॉर्न्स की संख्या भी 100 के पार पहुँच गई है। ``` **episode\_01\_metadata.json** ```json theme={null} { "filename": "episode_01.wav", "full_transcript": "नमस्ते, मैं हूँ रवि शर्मा...", "duration_seconds": 2847.5, "speaker_talk_time": { "SPEAKER_1": 1423.8, "SPEAKER_2": 1389.2 }, "segments": [ { "segment_id": 0, "speaker_id": 1, "start_time": 0.0, "end_time": 6.8, "text": "नमस्ते, मैं हूँ रवि शर्मा..." } ] } ``` *** **Related docs:** [Batch STT Introduction](/api/STTBatch/Introduction) · [Create Job](/api/STTBatch/Create_Job) · [Start Job](/api/STTBatch/Start_Job) · [Get Job Status](/api/STTBatch/Get_Job) · [Get Job Files](/api/STTBatch/Get_Job_Files) # Real-Time Quality & Compliance Monitoring Source: https://docs.gnani.ai/api/use-cases/real-time-compliance Build a production-grade monitoring system that streams contact center audio to the Gnani Prisma v2.5 WebSocket STT API, detects compliance violations and quality signals in live transcripts, and triggers alerts in under 200ms of speech completion. ## Overview Contact centers handling financial services, insurance, or healthcare operate under strict regulatory requirements. Agents must follow scripts, disclose specific information, and avoid prohibited language. Traditional QA reviews 2–5% of calls after the fact. By the time a violation is caught, it has already happened hundreds of times. This guide shows you how to build a system that monitors every call in real time. Audio streams to the Gnani Prisma v2.5 WebSocket STT API. Transcripts arrive within milliseconds of speech completion. A compliance and quality engine processes each segment, matches against rule sets, and fires alerts to your backend — while the call is still live. | Capability | Implementation | | ------------------------ | ------------------------------------------------------------------------------------------- | | **Live transcription** | WebSocket stream to `wss://api.vachana.ai/stt/v3/stream` with per-segment transcript events | | **Compliance detection** | Keyword and phrase matching on each `transcript` event with configurable rule sets | | **Quality monitoring** | Silence detection, interruption tracking, escalation phrase matching from segment metadata | | **Real-time alerts** | Async alert dispatcher — webhook, queue, or supervisor dashboard | | **Reconnect handling** | Exponential backoff with session continuity across drops | **Which API to use?** This use case uses the **WebSocket STT API** for real-time streaming. For post-call batch analysis, see the [Call Analytics Pipeline](/api/use-cases/call-analytics). *** ## Architecture The system has three logical layers: audio ingestion, transcription, and monitoring. Each runs concurrently in an async event loop. ```text theme={null} AUDIO SOURCE │ (Telephony bridge / RTP tap / microphone) │ PCM 16-bit LE, 16kHz or 8kHz, mono ↓ AUDIO STREAMER │ Chunks audio into 1024-byte frames (32ms @ 16kHz) │ Maintains real-time cadence — no burst, no starvation ↓ VACHANA WEBSOCKET STT API wss://api.vachana.ai/stt/v3/stream │ VAD detects speech boundaries │ Returns: connected → processing → transcript events │ Latency: ~300–500ms from end of speech to transcript ↓ TRANSCRIPT HANDLER → COMPLIANCE ENGINE → QUALITY ENGINE ↓ ALERT DISPATCHER → Webhook / Queue / Supervisor dashboard ``` Each call owns an isolated **session object** that tracks the full transcript buffer, a timeline of events, compliance status, quality metrics, and reconnect context. This state survives WebSocket reconnects and is flushed to your store at call end. *** ## Prerequisites | Requirement | Details | | ----------------- | ------------------------------------------------------------------------------------------------------ | | **Gnani API key** | Available from the Gnani API dashboard. Used as the `x-api-key-id` header on the WebSocket connection. | | **Python 3.10+** | Required by the SDK. The full example uses `asyncio`, `dataclasses`, and typed event classes. | | **Audio source** | PCM 16-bit LE, mono. Either 8kHz (PSTN/legacy VoIP) or 16kHz (wideband VoIP). Defaults to 16kHz. | | **Alert target** | An HTTP endpoint, message queue, or Redis channel to receive alerts. | ```bash theme={null} pip install gnani-vachana aiohttp python-dotenv ``` *** ## Authentication Authentication is performed at connection time via HTTP headers on the WebSocket upgrade request. There is no separate auth step — the connection either opens or returns 401. | Header | Required | Description | | --------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `x-api-key-id` | Yes | Your Gnani Prisma v2.5 API key. | | `lang_code` | Yes | BCP-47 language code. Defaults to `en-IN`. | | `x-sample-rate` | No | Audio sample rate in Hz. Accepted: `8000`, `16000`, `44100`, `48000`. Defaults to `16000`. | | `x-format` | No | Set `transcribe` for ITN (numbers, currency, dates in written form). ITN applies to `hi-IN` and `en-IN` only. | ```bash .env theme={null} GNANI_API_KEY=your-api-key-here ALERT_WEBHOOK_URL=https://supervisor.internal/alerts LANG_CODE=hi-IN SAMPLE_RATE=16000 ``` **Never hardcode API keys.** Load credentials from environment variables or a secrets manager. The `x-api-key-id` header is visible in plaintext in WebSocket upgrade logs — ensure those logs are access-controlled. *** ## End-to-End Workflow Your telephony bridge fires a call-start event. The monitor opens a WebSocket to `wss://api.vachana.ai/stt/v3/stream` with auth headers and language config. A session object is created and keyed to the call ID. The server returns a `connected` event confirming sample rate and chunk size. Any mismatch (wrong sample rate, unsupported language) surfaces immediately. An async producer task reads PCM frames from the telephony tap and sends them at real-time cadence: one 1024-byte frame every 32ms for 16kHz audio. Bursting frames degrades VAD accuracy. When VAD detects end-of-speech, the server sends a `processing` event. Use this timestamp to measure speech-to-transcript latency and to start a silence timer in the quality engine. The `transcript` event carries `text`, `segment_index`, `audio_duration_ms`, and `latency`. Both engines process the text synchronously. Alerts are dispatched async so they never block the next transcript. Compliance violations and quality alerts go to the alert dispatcher. Severity determines the channel: `CRITICAL` hits the supervisor dashboard immediately; `WARNING` queues for post-call review. On call end, close the WebSocket gracefully. Run final session-level checks (e.g. required disclosure was never spoken). Flush session state to your store and emit a call-complete summary event. *** ## Connecting to the WebSocket API The SDK's `GnaniSTTStreamClient` wraps the WebSocket connection, frame pacing, and event parsing. Use it as an async context manager. ```python basic connection theme={null} import asyncio, os from gnani.stt import GnaniSTTStreamClient async def open_stream(): async with GnaniSTTStreamClient( api_key=os.getenv("GNANI_API_KEY"), language_code="hi-IN", sample_rate=16000, ) as stream: async for event in stream: await handle_event(event) ``` *** ## Streaming Audio ### Audio format requirements | Property | 16kHz (wideband VoIP) | 8kHz (PSTN / legacy) | | ----------------- | ------------------------------- | ------------------------------- | | **Encoding** | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian | | **Channels** | 1 (mono) | 1 (mono) | | **Frame size** | 1024 bytes (512 samples = 32ms) | 1024 bytes (512 samples = 64ms) | | **x-sample-rate** | `16000` | `8000` | Each WebSocket frame must be exactly **1024 bytes**. Bursting frames (sending faster than real time) degrades VAD accuracy — the VAD model is trained on real-time cadence. ```python audio producer task theme={null} import asyncio FRAME_SIZE = 1024 # bytes — exactly 512 x 16-bit samples FRAME_MS_16K = 0.032 # 32ms per frame at 16kHz FRAME_MS_8K = 0.064 # 64ms per frame at 8kHz async def stream_audio_producer(stream, audio_source, sample_rate=16000, stop_event=None): frame_interval = FRAME_MS_16K if sample_rate == 16000 else FRAME_MS_8K buffer = bytearray() async for chunk in audio_source: if stop_event and stop_event.is_set(): break buffer.extend(chunk) while len(buffer) >= FRAME_SIZE: await stream.send_audio(bytes(buffer[:FRAME_SIZE])) buffer = buffer[FRAME_SIZE:] await asyncio.sleep(frame_interval) # enforce real-time cadence # Flush remaining partial frame padded with silence if buffer: await stream.send_audio(bytes(buffer) + b"\x00" * (FRAME_SIZE - len(buffer))) ``` *** ## WebSocket Event Reference | Event type | When sent | Key fields | | ------------ | ----------------------------------------------- | ---------------------------------------------------------------------------------- | | `connected` | Once, immediately after handshake. | `message`, `config.sample_rate`, `config.chunk_size`, `timestamp` | | `processing` | Each time VAD detects end-of-speech. | `timestamp` | | `transcript` | After transcription of a VAD segment completes. | `text`, `segment_index`, `segment_id`, `audio_duration_ms`, `latency`, `timestamp` | | `error` | Server-side error, recoverable or fatal. | `message`, `timestamp` | ```json transcript event theme={null} { "type": "transcript", "timestamp": "2024-01-15T10:30:05.987Z", "text": "guaranteed returns milenge, bilkul risk-free hai", "audio_duration_ms": 2340, "segment_id": "seg_7f3a92", "segment_index": 4, "latency": 318 } ``` The `latency` field (milliseconds from end of speech to transcript delivery) is your primary observability metric for pipeline health. Track p50, p95, p99 per call session and alert if p95 consistently exceeds your SLA threshold. *** ## Compliance Detection The compliance engine runs on each `transcript` event. It checks segment text against three rule categories: prohibited keywords, risk phrases, and required disclosures. All checks are synchronous string operations — they complete in under 1ms per segment. ```json rules/compliance.json theme={null} { "prohibited_keywords": [ { "rule_id": "PROH_001", "severity": "CRITICAL", "keywords": ["guaranteed returns", "guaranteed profit", "no risk", "risk-free"], "description": "SEBI-prohibited investment language" }, { "rule_id": "PROH_002", "severity": "CRITICAL", "keywords": ["personal account", "off the books", "my account"], "description": "Agent directing customer to off-channel transaction" } ], "risk_phrases": [ { "rule_id": "RISK_001", "severity": "WARNING", "phrases": ["cancel my policy", "close my account", "policy cancel"], "description": "Churn risk signal" }, { "rule_id": "RISK_002", "severity": "WARNING", "phrases": ["legal action", "consumer forum", "RBI complaint", "complaint"], "description": "Regulatory complaint intent" } ], "required_disclosures": [ { "rule_id": "DISC_001", "severity": "CRITICAL", "must_contain_one_of": ["this call is being recorded", "call recording", "recorded for quality"], "check_within_segments": 3, "description": "Recording disclosure required within first 3 segments" } ] } ``` ```python ComplianceEngine theme={null} import json from pathlib import Path from typing import List, Dict class ComplianceEngine: def __init__(self, rules_path="rules/compliance.json"): rules = json.loads(Path(rules_path).read_text()) self.prohibited = rules.get("prohibited_keywords", []) self.risk_phrases = rules.get("risk_phrases", []) self.disclosures = rules.get("required_disclosures", []) self._disclosed = set() def check(self, segment) -> List[Dict]: text = segment.text.lower() hits = [] for rule in self.prohibited: for kw in rule["keywords"]: if kw in text: hits.append({"rule_id": rule["rule_id"], "severity": rule["severity"], "matched": kw, "description": rule["description"], "segment_idx": segment.segment_index, "text": segment.text}) break for rule in self.risk_phrases: for phrase in rule["phrases"]: if phrase in text: hits.append({"rule_id": rule["rule_id"], "severity": rule["severity"], "matched": phrase, "description": rule["description"], "segment_idx": segment.segment_index, "text": segment.text}) break for rule in self.disclosures: rid = rule["rule_id"] if rid in self._disclosed: continue if any(p in text for p in rule["must_contain_one_of"]): self._disclosed.add(rid) elif segment.segment_index >= rule["check_within_segments"]: hits.append({"rule_id": rid, "severity": rule["severity"], "matched": "MISSING_DISCLOSURE", "description": rule["description"], "segment_idx": segment.segment_index, "text": ""}) self._disclosed.add(rid) return hits ``` *** ## Quality Monitoring ```json rules/quality.json theme={null} { "silence": { "threshold_seconds": 8 }, "escalation_phrases": [ "transfer to supervisor", "let me escalate", "i will get my supervisor" ], "interruption": { "min_duration_ms": 300 }, "short_segment_ms": 500 } ``` ```python QualityEngine theme={null} import json from datetime import datetime, timezone from pathlib import Path from typing import List, Dict, Optional class QualityEngine: def __init__(self, rules_path="rules/quality.json"): rules = json.loads(Path(rules_path).read_text()) self.silence_threshold = rules["silence"]["threshold_seconds"] self.escalation_phrases = [p.lower() for p in rules["escalation_phrases"]] self.interruption_ms = rules["interruption"]["min_duration_ms"] self.short_segment_ms = rules["short_segment_ms"] self._last_processing_ts: Optional[datetime] = None def on_processing(self, timestamp_str: str): self._last_processing_ts = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) def check(self, session, segment) -> List[Dict]: now, text, events = datetime.now(timezone.utc), segment.text.lower(), [] if session.last_segment_end: silence_s = (now - session.last_segment_end).total_seconds() - (segment.audio_duration_ms / 1000) if silence_s > self.silence_threshold: events.append({"event_type": "SILENCE", "severity": "WARNING", "silence_s": round(silence_s, 1), "segment_idx": segment.segment_index, "description": f"Silence gap of {silence_s:.1f}s detected"}) for phrase in self.escalation_phrases: if phrase in text: events.append({"event_type": "ESCALATION", "severity": "WARNING", "matched": phrase, "segment_idx": segment.segment_index, "description": "Supervisor escalation signal"}) break if self._last_processing_ts and segment.audio_duration_ms < self.short_segment_ms: gap_ms = (now - self._last_processing_ts).total_seconds() * 1000 if gap_ms < self.interruption_ms: events.append({"event_type": "INTERRUPTION", "severity": "INFO", "gap_ms": round(gap_ms, 1), "segment_idx": segment.segment_index, "description": f"Possible interruption — {gap_ms:.0f}ms gap"}) return events ``` *** ## Error Handling & Reconnect Logic WebSocket connections drop. The reconnect loop below uses exponential backoff with full jitter and caps at a configurable maximum. Session state is preserved across reconnects using `processed_indices` to deduplicate segments. ```python reconnect loop theme={null} import asyncio, random, os from gnani.stt import GnaniSTTStreamClient, StreamConnectionError, StreamClosedError, StreamError MAX_RECONNECTS = 5 BASE_BACKOFF_S = 1.0 MAX_BACKOFF_S = 30.0 async def monitor_call_with_reconnect(session, audio_source, compliance_engine, quality_engine, alert_dispatcher): attempt = 0 while attempt <= MAX_RECONNECTS: try: async with GnaniSTTStreamClient( api_key=os.getenv("GNANI_API_KEY"), language_code=session.language_code, sample_rate=int(os.getenv("SAMPLE_RATE", "16000")), ) as stream: if attempt > 0: session.reconnect_count += 1 attempt = 0 # reset backoff counter on successful connect stop_event = asyncio.Event() producer = asyncio.create_task(stream_audio_producer(stream, audio_source, stop_event=stop_event)) async for event in stream: await handle_event(session, event, compliance_engine, quality_engine, alert_dispatcher) stop_event.set() await producer return # clean exit except StreamConnectionError: print(f"[{session.call_id}] Auth failure. Not retrying.") raise except (StreamClosedError, ConnectionResetError, OSError) as e: attempt += 1 if attempt > MAX_RECONNECTS: raise backoff = min(BASE_BACKOFF_S * (2 ** attempt), MAX_BACKOFF_S) jitter = random.uniform(0, backoff * 0.2) print(f"[{session.call_id}] Reconnect {attempt}/{MAX_RECONNECTS} in {backoff+jitter:.1f}s") await asyncio.sleep(backoff + jitter) ``` | Error | Cause | Strategy | | -------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- | | `StreamConnectionError` | 401, invalid API key, unsupported language code. | Do not retry. Fix config and redeploy. | | `StreamClosedError` | Server closed cleanly (service restart, session timeout). | Retry with backoff. Session state is preserved. | | `ConnectionResetError / OSError` | Network drop, TCP reset, intermediary timeout. | Exponential backoff + jitter. Cap at `MAX_RECONNECTS`. | | `StreamError` | STT engine failure reported in an `error` event. | Log, retry once. Flag the call for manual review on repeat failures. | *** ## Production Best Practices Each active call runs in its own `asyncio.Task`. The audio producer and event consumer run concurrently within that task. Do not use threads — the WebSocket library is async-native. A single well-tuned Python process handles 100+ concurrent calls comfortably; the bottleneck is network I/O, not CPU. Compliance and quality checks run synchronously (sub-millisecond string matching). Alert dispatch — HTTP webhooks, queue publishes, database writes — must always be fire-and-forget via `asyncio.create_task()`. A slow downstream system under load must never delay the next transcript event. | Optimization | Impact | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Co-locate with telephony bridge | Run the monitor in the same region as the Gnani Prisma v2.5 API. Cross-region adds 50–150ms RTT per frame delivery. | | 16kHz over 8kHz when possible | Higher accuracy transcripts mean fewer false positives in compliance matching. | | Pre-compile compliance patterns | Compile all regex at engine `__init__`. Never compile inside the hot path. | | Buffer writes, not reads | Write to an in-memory session buffer. Flush to the database at call end or on CRITICAL alerts only. | | Metric | Source | | ----------------------- | -------------------------------------------------------------- | | `transcript_latency_ms` | `latency` field on each `transcript` event. Track p50/p95/p99. | | `segment_count` | Increment on each `transcript` event. | | `compliance_hit_rate` | Compliance hits / total segments per call. | | `silence_gap_seconds` | Max silence gap derived from `processing` event timestamps. | | `reconnect_count` | `session.reconnect_count`, incremented on each reconnect. | *** ## Debugging | Symptom | Cause | Fix | | ---------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Connection immediately closes — no `connected` event | Invalid API key, wrong `lang_code`, missing required headers. | Log the WebSocket close code — 4001 = auth failure. | | Transcripts arrive but text is empty or garbled | `x-sample-rate` does not match the actual audio sample rate. Audio is not mono PCM. | Run `ffprobe` on the source. Convert stereo to mono before streaming. | | VAD fires too often — sentences cut mid-utterance | Frames being burst-sent faster than real time. | Enforce `asyncio.sleep(frame_interval)` after every send. | | VAD never fires — no processing or transcript events | Audio buffer is all zeros. Audio source is not connected. | Print `frame[:32].hex()`. All zeros = silent source. | | Compliance rules fire on unrelated text | Substring match without word boundaries. | Switch to word-boundary regex. Lowercase and strip punctuation before matching. | | Duplicate alerts on reconnect | Session buffer re-processed after reconnect. | Check `segment_index in session.processed_indices` before dispatching any alert. | *** ## Full Runnable Example ```python monitor.py theme={null} """ monitor.py — Real-time quality and compliance monitoring pipeline. Usage: GNANI_API_KEY=your-key python monitor.py --audio call.pcm --lang hi-IN GNANI_API_KEY=your-key python monitor.py --audio call.pcm --lang en-IN --rate 8000 Install: pip install gnani-vachana aiohttp python-dotenv """ import asyncio, json, os, random, argparse, aiohttp from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path from typing import List, Dict, Optional, Set, AsyncIterator from dotenv import load_dotenv from gnani.stt import ( GnaniSTTStreamClient, StreamConnectedEvent, StreamProcessingEvent, StreamTranscriptEvent, StreamErrorEvent, StreamConnectionError, StreamClosedError, StreamError, ) load_dotenv() FRAME_SIZE = 1024 MAX_RECONNECTS = 5 BASE_BACKOFF_S = 1.0 MAX_BACKOFF_S = 30.0 FRAME_MS_16K = 0.032 FRAME_MS_8K = 0.064 @dataclass class TranscriptSegment: segment_index: int text: str audio_duration_ms: int latency_ms: int timestamp: datetime compliance_flags: List[str] = field(default_factory=list) quality_flags: List[str] = field(default_factory=list) @dataclass class CallSession: call_id: str language_code: str started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) segments: List[TranscriptSegment] = field(default_factory=list) last_segment_end: Optional[datetime] = None reconnect_count: int = 0 processed_indices: Set[int] = field(default_factory=set) def add_segment(self, event) -> TranscriptSegment: seg = TranscriptSegment( segment_index=event.segment_index, text=event.text, audio_duration_ms=event.audio_duration_ms, latency_ms=event.latency, timestamp=datetime.now(timezone.utc), ) self.segments.append(seg) self.processed_indices.add(event.segment_index) self.last_segment_end = datetime.now(timezone.utc) return seg async def stream_audio_producer(stream, audio_source, stop_event: asyncio.Event): buffer = bytearray() async for chunk in audio_source: if stop_event.is_set(): break buffer.extend(chunk) while len(buffer) >= FRAME_SIZE: await stream.send_audio(bytes(buffer[:FRAME_SIZE])) buffer = buffer[FRAME_SIZE:] if buffer: await stream.send_audio(bytes(buffer) + b" " * (FRAME_SIZE - len(buffer))) async def handle_event(session, event, compliance_engine, quality_engine, alert_dispatcher): if isinstance(event, StreamConnectedEvent): print(f"[{session.call_id}] Connected sample_rate={event.sample_rate}") elif isinstance(event, StreamProcessingEvent): quality_engine.on_processing(event.timestamp) elif isinstance(event, StreamTranscriptEvent): if event.segment_index in session.processed_indices: return # deduplicate across reconnects seg = session.add_segment(event) print(f"[{session.call_id}][{seg.segment_index}] {seg.text} (latency={seg.latency_ms}ms)") c_hits = compliance_engine.check(seg) q_events = quality_engine.check(session, seg) seg.compliance_flags = [h["rule_id"] for h in c_hits] seg.quality_flags = [e["event_type"] for e in q_events] if c_hits or q_events: asyncio.create_task(alert_dispatcher.send(session, seg, c_hits + q_events)) elif isinstance(event, StreamErrorEvent): raise RuntimeError(f"STT error: {event.message}") async def monitor_call(session, audio_source, compliance_engine, quality_engine, alert_dispatcher): attempt = 0 while attempt <= MAX_RECONNECTS: try: async with GnaniSTTStreamClient( api_key=os.getenv("GNANI_API_KEY"), language_code=session.language_code, sample_rate=int(os.getenv("SAMPLE_RATE", "16000")), ) as stream: if attempt > 0: session.reconnect_count += 1 attempt = 0 stop_event = asyncio.Event() producer = asyncio.create_task(stream_audio_producer(stream, audio_source, stop_event)) async for event in stream: await handle_event(session, event, compliance_engine, quality_engine, alert_dispatcher) stop_event.set() await producer return except StreamConnectionError: raise except (StreamClosedError, ConnectionResetError, OSError) as e: attempt += 1 if attempt > MAX_RECONNECTS: raise backoff = min(BASE_BACKOFF_S * (2 ** attempt), MAX_BACKOFF_S) print(f"[{session.call_id}] Reconnect {attempt}/{MAX_RECONNECTS} in {backoff:.1f}s {e}") await asyncio.sleep(backoff + random.uniform(0, backoff * 0.2)) except StreamError as e: attempt += 1 print(f"[{session.call_id}] Server error: {e.message}") await asyncio.sleep(BASE_BACKOFF_S) async def file_audio_source(path: str, sample_rate=16000) -> AsyncIterator[bytes]: frame_interval = FRAME_MS_16K if sample_rate == 16000 else FRAME_MS_8K with open(path, "rb") as f: while chunk := f.read(FRAME_SIZE): yield chunk await asyncio.sleep(frame_interval) async def main(): parser = argparse.ArgumentParser() parser.add_argument("--audio", required=True) parser.add_argument("--lang", default="hi-IN") parser.add_argument("--rate", default=16000, type=int) args = parser.parse_args() os.environ["SAMPLE_RATE"] = str(args.rate) session = CallSession(call_id="CALL_001", language_code=args.lang) audio = file_audio_source(args.audio, sample_rate=args.rate) await monitor_call(session, audio, ComplianceEngine(), QualityEngine(), AlertDispatcher()) duration = (datetime.now(timezone.utc) - session.started_at).total_seconds() print(f"Duration: {duration:.1f}s | Segments: {len(session.segments)}") print(f"Compliance hits: {sum(len(s.compliance_flags) for s in session.segments)}") print(f"Quality events: {sum(len(s.quality_flags) for s in session.segments)}") if __name__ == "__main__": asyncio.run(main()) ``` *** ## What to Build Next * **Speaker Diarization** — Separate agent and customer voices. Attribute compliance hits to the correct speaker. * **Sentiment Analysis** — Feed each segment's text to a sentiment model. Track the sentiment arc across the call. * **Agent Assist** — On each `transcript` event, call an LLM with the running conversation context to surface next-best-action suggestions in real time. * **LLM Summarisation** — At call end, send the full session transcript to an LLM for structured output: issue, resolution, action items, disposition. * **Compliance Scoring** — Build a per-call compliance score (0–100) based on rule severity, frequency, and placement in the call. **Related docs:** [WebSocket STT API](/api/STT/stt-websocket) · [Batch STT for post-call analysis](/api/STTBatch/Introduction) · SDK install: `pip install gnani-vachana` # Creating a Workforce Source: https://docs.gnani.ai/creating-a-workforce ## What is Workforce? Workforce allows you to create a **multi-agent workflow** where multiple AI voice agents work together to handle different parts of a conversation. Instead of building one large agent that handles every possible scenario, Workforce lets you connect multiple specialized agents together. Each agent focuses on a specific task, and conversations move between them based on defined conditions. For example: * A Greeting Agent that welcomes the user and gathers basic information * A Support Agent that answers product questions * A Cancellation Agent that handles refund or cancellation requests By distributing responsibilities across agents, workflows become easier to maintain, more scalable and more reliable. Think of Workforce as building a team of AI employees working together in a coordinated flow. *** ## Why Use Workforce? A single agent managing too many tasks can become: * Hard to maintain * Difficult to debug * Less reliable in complex scenarios Workforce solves this by enabling specialized agents connected through logic-based routing. Benefits include: 1. Modular Design: Each agent handles a specific responsibility. 2. Better Maintainability: Update one agent without affecting others. 3. Scalable Workflows: Add new agents as your use case grows. 4. Smarter Routing: Conversations move automatically between agents based on conditions. *** ## Key Concepts ### Workforce Canvas The Workforce Canvas is the visual workspace where you design your multi-agent flow. On the canvas you can: * Add agents * Connect agents together * Define conditions for transferring conversations Each element on the canvas is represented as a node. ### Agent Nodes Agent nodes represent the individual AI agents that handle conversations. Each agent performs a specific role in the workflow. Agents added to the workforce are imported from your existing agent library. ### Edges (Connections) Edges connect agents together and define how conversations move from one agent to another. Each connection can include a condition that determines when the handoff should occur. ### Trigger Node The Trigger Node is the starting point of the workforce. As of now, every workforce begins with an Outbound Call Trigger that is automatically added to the canvas when the workforce is created. This trigger represents the starting point of the workflow. From this node, you can connect the first agent that should handle the conversation. *** ## A. Creating a Workforce 1. Navigate to **Build ->** **Workforce** section in the platform. 2. Click **+ Workforce** button. 3. You will be asked to provide the name and description. 4. After entering the details, click **Create**. 5. You will be redirected to the **Workforce Canvas**. A trigger node will already be present on the canvas.  *** ## B. Adding Agents to the Workforce Agents used inside a workforce are imported from your existing agent library. ### How to Import an Agent 1. On the Workforce Canvas, select the option to add an agent. 2. Select an agent from your agent library. 3. Confirm the selection. The selected agent will appear as a node on the canvas. ### Linked Agent Behavior Agents imported into a workforce are **linked to the original agent in the Manage Agents library**. This means: * The agent configuration cannot be edited from the workforce canvas. * Changes made to the agent in the Manage Agents page will automatically apply to all workforces where the agent is used. ### Editing an Imported Agent To modify the agent configuration: 1. Navigate to Manage Agents. 2. Open the agent. 3. Make the required changes. 4. Save the agent. The updated behavior will automatically reflect in all workforces where the agent is imported. *** ## C. Connecting Agents Agents must be connected together to define how conversations move through the workflow. ### Creating a Connection 1. Drag a connector from Agent A to Agent B. 2. A connection will be created between the two agents. After the connection is created, you can define the condition that determines when the handoff should occur. *** ## D. Conditional Handoff Between Agents Conditional handoff allows conversations to move between agents when specific conditions are met. These conditions are written in natural language. ### How to Configure a Conditional Handoff 1. Drag a connector from Agent A to Agent B. 2. A label saying Add condition appears on the connection. 3. Click the label. 4. A configuration sidebar opens. 5. Enter the condition and edge label. 6. Save the configuration. ### Example Conditions Examples of natural language conditions include: * "User asks for a supervisor" * "User wants to cancel the appointment" * "Sentiment is negative and the user mentions cancel" ### How the Handoff Works During a Call If the condition defined on the connection is met during the conversation: * The conversation will automatically transfer to the next agent. If the condition is never triggered: * The transfer will not occur. ### Data Passed During Handoff When a conversation is transferred between agents, the system passes the complete context to the next agent, including: * Conversation transcript * Structured data extracted during the call * Session context This allows the next agent to continue the conversation without losing information. *** ## Best Practices ### Use Specialized Agents Each agent should handle a specific responsibility. Avoid building agents that handle **too many tasks**. Example: * Greeting Agent * Support Agent * Cancellation Agent This keeps workflows simpler and easier to maintain. ### Write Clear Handoff Conditions Use clear and specific conditions when defining agent transitions. Examples: * "User wants to cancel subscription" * "User requests appointment rescheduling" * "User asks to speak to supervisor" Avoid vague conditions. ### Reuse Agents Across Workforces If multiple workflows require the same agent behavior, reuse the same agent by importing it into different workforces. Any improvements made to that agent will automatically apply everywhere it is used. *** ## Summary The Workforce feature allows you to design multi-agent workflows where specialized agents collaborate to handle conversations. With Workforce you can: * Create workflows composed of multiple agents * Visually design conversation flows * Transfer conversations between agents based on conditions * Reuse agents across multiple workflows This modular approach makes complex voice automation systems easier to build, manage, and scale. # Introduction – Agent Builder Source: https://docs.gnani.ai/introduction Build, configure, test, and deploy voice agents with knowledge bases, integrations, and analytics. Agent Builder is the console for creating voice agents that handle customer calls. You define what the agent knows, how it speaks, which tools it can use, and how you measure results — without writing application code for the core flow. | Need | Documentation | | ------------------------------------- | ------------------------------------------------------------- | | Speech APIs (STT/TTS) in your own app | [Gnani Speech APIs](/api/introduction/introduction) | | Create or update agents over REST | [Agent Builder Platform API](/Platform/platform-introduction) | ## How an agent is built An agent brings together four pieces: | Piece | Role | | -------------- | ------------------------------------------------------ | | Knowledge base | Documents and URLs the agent can reference | | Agent | Prompt, voice, ASR/LLM/TTS settings, and call behavior | | Integrations | SMS, email, CRM, ticketing, and custom APIs | | Analytics | Logs, dispositions, and performance metrics | ## Recommended path Work through these steps in order: | Step | Task | Guide | | ---- | ----------------------------- | ------------------------------------------------ | | 1 | Create a knowledge base | [Creating a Knowledge Base](/A01_KB) | | 2 | Create and configure an agent | [Creating Your First Agent](/A02_Agent) | | 3 | Test chat and voice | [Testing Your Agent](/A03_Testing) | | 4 | Whitelist phone numbers | [Whitelisting Numbers](/A04_Whitelisting) | | 5 | Add integrations | [Twilio SMS](/D01_Twilio_SMS) and related guides | | 6 | Review calls and metrics | [Conversational Logs](/E01_Conversational_Logs) | ## Advanced configuration | Topic | Guide | | ----------------------------------- | ----------------------------------------------------- | | Dynamic variables and pre-call data | [Dynamic Variables](/B04_Dynamic_Variables) | | Multilingual behavior | [Language Switch](/C02_Language_Switch) | | Call dispositions | [Writing a Disposition Prompt](/C01_Disposition) | | DTMF and ASR tuning | [DTMF](/B01_DTMF) · [Advanced ASR](/B02_Advanced_ASR) | ## Monitoring | Topic | Guide | | ----------------- | ----------------------------------------------- | | Conversation logs | [Conversational Logs](/E01_Conversational_Logs) | | Agent analytics | [Agent Analytics](/E03_Agent_Analytics) | | Action logs | [Action Logs](/E04_Action_Logs) | | Support | [Support Desk](/F01_Support) | *** ## Docs feedback Docs feedback: [Gnani Agents docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571) **Last verified:** 5 August 2026 # LiveKit Plugin Source: https://docs.gnani.ai/livekit/introduction Use Gnani Prisma (STT) and Timbre (TTS) inside LiveKit Agents voice pipelines for Indian languages. ## Overview `livekit-plugins-gnani` is a LiveKit Agents plugin that wraps the Gnani STT and TTS APIs into LiveKit's standard `stt.STT` and `tts.TTS` base classes. Drop it into any LiveKit voice agent pipeline and get high-accuracy Indian-language speech recognition and low-latency synthesis without managing WebSocket connections yourself. ```text theme={null} livekit-plugins-gnani ├── STT (Prisma): REST + WebSocket └── TTS (Timbre): REST + SSE + WebSocket ↑ Your LiveKit voice agent ``` This plugin implements the Gnani REST and WebSocket APIs directly using `aiohttp` (REST STT/TTS) and `websockets` (streaming STT/TTS). No external SDK is required — connection logic, authentication, and audio format handling are self-contained. Authentication uses a single `api_key` passed via the `X-API-Key-ID` header. This integration is maintained by [Gnani.ai](https://gnani.ai/). *** ## Installation ```bash theme={null} pip install livekit-plugins-gnani ``` This also installs [`websockets`](https://pypi.org/project/websockets/) and [`livekit-agents`](https://pypi.org/project/livekit-agents/) as dependencies. **Requirements:** Python 3.10+ *** ## Prerequisites You need a Gnani API key. [Gnani APIs](https://app.gnani.ai/voice) All APIs require a single API key — no `organization_id` or `user_id` needed. ```bash theme={null} export GNANI_API_KEY="your-api-key" ``` | Variable | Purpose | | --------------- | ----------------------------- | | `GNANI_API_KEY` | API key for Gnani STT and TTS | **Or pass the key in the constructor:** ```python theme={null} stt = STT(api_key="your-api-key", language="hi-IN") tts = TTS(api_key="your-api-key") ``` If upgrading from an earlier version, remove any `organization_id` and `user_id` parameters — they are no longer accepted. *** ## Services This plugin provides STT and TTS classes with multiple transports. Choose based on your use case: | Class | Type | Transport | Best for | | ----- | ---- | --------------------------------------------------------- | ---------------------------------------------- | | `STT` | STT | REST (`recognize()`) | File-based transcription via `POST /stt/v3` | | `STT` | STT | WebSocket (`stream()`) | Live conversations, real-time agents | | `TTS` | TTS | REST (`synthesize_method="rest"`) | Batch synthesis, non-streaming pipelines | | `TTS` | TTS | SSE (`synthesize_method="sse"`) | Chunked synthesis with lower latency than REST | | `TTS` | TTS | WebSocket (`synthesize_method="websocket"` or `stream()`) | Lowest-latency synthesis for live agents | *** ## Quick Start ### Speech-to-Text (REST + Streaming) ```python theme={null} from livekit.plugins.gnani import STT stt = STT(language="hi-IN") # REST STT (file-based transcription) speech_event = await stt.recognize(audio_buffer) # Streaming STT (real-time WebSocket) speech_stream = stt.stream() ``` ### Text-to-Speech (REST) ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS(voice="Pranav") ``` ### Text-to-Speech (SSE Streaming) ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS(voice="Pranav", synthesize_method="sse") ``` ### Text-to-Speech (WebSocket Streaming) ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS(voice="Pranav", synthesize_method="websocket") ``` All three TTS modes work with the standard LiveKit voice agent pipeline. The `synthesize_method` controls which transport `synthesize()` uses (REST, SSE, or WebSocket). The `stream()` method always uses WebSocket regardless of this setting. *** ## STT — Speech-to-Text (REST) File-based transcription via REST. * Calls `POST /stt/v3` * Suitable for pre-recorded audio and file-based pipelines * Invoked through `stt.recognize(audio_buffer)` ```python theme={null} from livekit.plugins.gnani import STT stt = STT( language="hi-IN", sample_rate=16000, format="verbatim", ) speech_event = await stt.recognize(audio_buffer) ``` ### Settings | Parameter | Type | Default | Description | | --------------------- | -------- | -------------------------- | ---------------------------------------------------------------------- | | `language` | `string` | `"en-IN"` | BCP-47 language code. See [Supported Languages](#supported-languages). | | `sample_rate` | `int` | `16000` | Audio sample rate in Hz. Accepted values: `8000`, `16000`. | | `format` | `string` | `"verbatim"` | Output format. Use `"transcribe"` to enable ITN. | | `itn_native_numerals` | `bool` | `False` | Keep native numerals when ITN is enabled. | | `api_key` | `string` | `None` | API key. Defaults to `GNANI_API_KEY` env var. | | `base_url` | `string` | `"https://api.vachana.ai"` | API base URL. | *** ## STT — Speech-to-Text (WebSocket) Real-time streaming speech-to-text via WebSocket with VAD. * Connects to `wss://api.vachana.ai/stt/v3/stream` * Sends raw PCM audio in 1,024-byte frames * Supports 8 kHz and 16 kHz sample rates * Invoked through `stt.stream()` ```python theme={null} from livekit.plugins.gnani import STT stt = STT( language="hi-IN", sample_rate=16000, # 8000 or 16000 ) speech_stream = stt.stream() ``` ### Streaming PCM Specification All streaming audio must be sent as **raw PCM binary frames** — no container format (WAV, MP3) mid-stream. | Property | 16 kHz | 8 kHz | | ------------------- | --------------------------------------- | --------------------------------------- | | Encoding | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian | | Sample Rate | 16,000 Hz | 8,000 Hz | | Channels | 1 (mono) | 1 (mono) | | Samples per chunk | 512 | 512 | | **Bytes per frame** | **1,024 bytes** (512 samples × 2 bytes) | **1,024 bytes** (512 samples × 2 bytes) | | Frame duration | 32 ms | 64 ms | Frames must be sent at **real-time cadence**. See [STT Realtime — PCM Specification](/api/STT/stt-websocket#pcm-specification) for full details. ### Settings | Parameter | Type | Default | Description | | ------------- | -------- | -------------------------- | ---------------------------------------------------------------------- | | `language` | `string` | `"en-IN"` | BCP-47 language code. See [Supported Languages](#supported-languages). | | `sample_rate` | `int` | `16000` | Audio sample rate in Hz. Accepted values: `8000`, `16000`. | | `format` | `string` | `"verbatim"` | Output format. Use `"transcribe"` to enable ITN. | | `api_key` | `string` | `None` | API key. Defaults to `GNANI_API_KEY` env var. | | `base_url` | `string` | `"https://api.vachana.ai"` | API base URL. | *** ## TTS — Text-to-Speech (REST) REST-based text-to-speech for non-streaming use cases. Returns the complete audio in a single response. * Single-request batch synthesis via `synthesize_method="rest"` (default) * Suitable for batch synthesis or pipelines where streaming is not needed ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS( voice="Pranav", synthesize_method="rest", ) ``` ### Settings | Parameter | Type | Default | Description | | ------------------- | -------- | -------------------------- | ------------------------------------------------------------- | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `model` | `string` | `"timbre-v2.5"` | TTS model ID. | | `sample_rate` | `int` | `16000` | Output sample rate in Hz (`8000`, `16000`, `22050`, `44100`). | | `encoding` | `string` | `"linear_pcm"` | Audio encoding (`linear_pcm`, `oggopus`). | | `container` | `string` | `"wav"` | Container format (`wav`, `raw`, `mp3`, `mulaw`, `ogg`). | | `num_channels` | `int` | `1` | Number of audio channels. | | `bitrate` | `string` | `None` | Optional bitrate (`96k`, `128k`, `192k`). | | `synthesize_method` | `string` | `"rest"` | Transport for `synthesize()`. | | `api_key` | `string` | `None` | API key. Defaults to `GNANI_API_KEY` env var. | | `base_url` | `string` | `"https://api.vachana.ai"` | API base URL. | *** ## TTS — Text-to-Speech (SSE) Streaming text-to-speech via Server-Sent Events. Lower latency than REST. * Chunked synthesis via `synthesize_method="sse"` * Streams audio chunks as synthesis progresses ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS( voice="Pranav", synthesize_method="sse", ) ``` ### Settings | Parameter | Type | Default | Description | | ------------------- | -------- | --------------- | ------------------------------------------------------------ | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `model` | `string` | `"timbre-v2.5"` | TTS model ID. | | `sample_rate` | `int` | `16000` | Output sample rate in Hz. | | `encoding` | `string` | `"linear_pcm"` | Audio encoding (`linear_pcm`, `oggopus`). | | `container` | `string` | `"wav"` | Container format (`wav`, `raw`, `mp3`, `mulaw`, `ogg`). | | `synthesize_method` | `string` | `"sse"` | Set to `"sse"` for Server-Sent Events. | | `api_key` | `string` | `None` | API key. Defaults to `GNANI_API_KEY` env var. | *** ## TTS — Text-to-Speech (WebSocket, recommended) Streaming text-to-speech via WebSocket. Lowest latency for live conversational agents. * Use `synthesize_method="websocket"` for `synthesize()`, or call `stream()` (always WebSocket) * Ideal for live agents where latency matters * Change voice or model at runtime via `update_options()` ```python theme={null} from livekit.plugins.gnani import TTS tts = TTS( voice="Pranav", synthesize_method="websocket", ) ``` ### Settings | Parameter | Type | Default | Description | | ------------------- | -------- | --------------- | ------------------------------------------------------------ | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `model` | `string` | `"timbre-v2.5"` | TTS model ID. | | `sample_rate` | `int` | `16000` | Output sample rate in Hz. | | `encoding` | `string` | `"linear_pcm"` | Audio encoding (`linear_pcm`, `oggopus`). | | `container` | `string` | `"wav"` | Container format (`wav`, `raw`, `mp3`, `mulaw`, `ogg`). | | `synthesize_method` | `string` | `"websocket"` | Set to `"websocket"` for WS `synthesize()`. | | `api_key` | `string` | `None` | API key. Defaults to `GNANI_API_KEY` env var. | *** ## Available Voices To see the available voices, [click here](/api/TTS/available-voices). *** ## Supported Languages ### STT Languages (Speech-to-Text) STT uses BCP-47 locale codes (e.g. `hi-IN`, `bn-IN`). Note: STT uses the **`-IN`** suffix (unlike TTS). For the full list, see [STT — Supported Languages](/api/STT/speech-to-text#supported-languages) and [STT Realtime — Supported Languages](/api/STT/stt-websocket#supported-languages). ### TTS Languages (Text-to-Speech) For the full list of supported languages, see [TTS REST — Request Parameters](/api/TTS/tts-inference#request-parameters). *** ## Further Reading * [STT REST API](/api/STT/speech-to-text) — file-based transcription reference * [STT Realtime API](/api/STT/stt-websocket) — WebSocket protocol reference * [TTS REST API](/api/TTS/tts-inference) — synchronous synthesis reference * [TTS Streaming (SSE)](/api/TTS/tts-sse) — SSE synthesis reference * [TTS Realtime API](/api/TTS/tts-websocket) — WebSocket TTS reference * [`livekit-plugins-gnani` on PyPI](https://pypi.org/project/livekit-plugins-gnani/) — this plugin * [LiveKit Agents Docs](https://docs.livekit.io/agents/) — LiveKit framework reference * [Gnani STT Plugin Guide](https://docs.livekit.io/agents/integrations/stt/gnani/) — LiveKit STT integration * [Gnani TTS Plugin Guide](https://docs.livekit.io/agents/integrations/tts/gnani/) — LiveKit TTS integration # Pipecat Plugin Source: https://docs.gnani.ai/pipecat/introduction Use Gnani STT and TTS inside Pipecat voice agent pipelines for Indian languages. ## Overview `pipecat-gnani` is a Pipecat service integration that wraps the Gnani STT and TTS APIs into Pipecat's standard `SegmentedSTTService`, `STTService`, `TTSService`, and `InterruptibleTTSService` base classes. Drop the services into any Pipecat pipeline and get high-accuracy Indian-language transcription and low-latency synthesis without managing WebSocket connections yourself. ```text theme={null} gnani-vachana (>=0.7.3) ← Core SDK on PyPI (import as `gnani`) ↑ pipecat-gnani ← This package (Pipecat service adapters) ├── STT: REST + WebSocket └── TTS: REST + SSE + WebSocket ↑ Your Pipecat voice agent ``` All connection logic, authentication, and audio format handling live in the core SDK. The plugin is purely an adapter layer. This integration is maintained by [Gnani.ai](https://gnani.ai/). Tested with **Pipecat v1.5.0**. *** ## Installation ```bash theme={null} pip install pipecat-gnani ``` Or with [uv](https://docs.astral.sh/uv/): ```bash theme={null} uv add pipecat-gnani ``` This also installs [`gnani-vachana`](https://pypi.org/project/gnani-vachana/) (>= 0.7.3) as a dependency. The Python import package name remains `gnani`. **Requirements:** Python 3.10+ *** ## Prerequisites You need a Gnani API key. [Gnani APIs](https://app.gnani.ai/voice) ```bash theme={null} export GNANI_API_KEY="your-api-key" ``` | Variable | Purpose | | --------------- | ---------------------------------------------------- | | `GNANI_API_KEY` | API key for Gnani STT and TTS | | `GROQ_API_KEY` | API key for the Groq LLM in the foundational example | *** ## Services This plugin provides five service classes. Choose based on your use case: | Service | Type | Transport | Base Class | Best for | | --------------------- | ---- | --------- | ------------------------- | ---------------------------------------------------------------------- | | `GnaniHttpSTTService` | STT | REST POST | `SegmentedSTTService` | File-based transcription via `POST /stt/v3`. Requires VAD in pipeline. | | `GnaniSTTService` | STT | WebSocket | `STTService` | Live conversations, real-time agents | | `GnaniHttpTTSService` | TTS | REST POST | `TTSService` | Batch synthesis, non-streaming pipelines | | `GnaniSSETTSService` | TTS | SSE | `TTSService` | Streaming synthesis with lower latency than REST | | `GnaniTTSService` | TTS | WebSocket | `InterruptibleTTSService` | Conversational agents with interruption support | *** ## Quick Start ### Pipeline snippet The snippet below shows the core `Pipeline([...])` wiring used in the foundational example. See [`examples/foundational/agent.py`](https://github.com/Gnani-AI-Mintlify/pipecat-gnani/blob/main/examples/foundational/agent.py) for the full runnable version. ```python theme={null} import os from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, LLMUserAggregatorParams, ) from pipecat.services.groq.llm import GroqLLMService from pipecat.transcriptions.language import Language from pipecat_gnani import GnaniSTTService, GnaniTTSService # transport = ... # see examples/foundational/agent.py stt = GnaniSTTService( api_key=os.environ["GNANI_API_KEY"], settings=GnaniSTTService.Settings(language=Language.HI_IN), ) tts = GnaniTTSService( api_key=os.environ["GNANI_API_KEY"], settings=GnaniTTSService.Settings(voice="Pranav"), ) llm = GroqLLMService( api_key=os.environ["GROQ_API_KEY"], settings=GroqLLMService.Settings( model="llama-3.1-8b-instant", ), ) context = LLMContext() aggregators = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), ) pipeline = Pipeline( [ transport.input(), stt, aggregators.user(), llm, tts, transport.output(), aggregators.assistant(), ] ) task = PipelineTask( pipeline, params=PipelineParams(enable_metrics=True), ) ``` Swap service classes in `agent.py` for REST or SSE variants — WebSocket STT + TTS is the default for lowest latency and interruption support. ### Speech-to-Text (REST) ```python theme={null} from pipecat_gnani import GnaniHttpSTTService from pipecat.transcriptions.language import Language stt = GnaniHttpSTTService( api_key="your-api-key", aiohttp_session=session, settings=GnaniHttpSTTService.Settings( language=Language.HI_IN, ), ) ``` ### Speech-to-Text (Streaming WebSocket) ```python theme={null} from pipecat_gnani import GnaniSTTService from pipecat.transcriptions.language import Language stt = GnaniSTTService( api_key="your-api-key", settings=GnaniSTTService.Settings( language=Language.HI_IN, ), ) ``` ### Text-to-Speech (REST) ```python theme={null} from pipecat_gnani import GnaniHttpTTSService tts = GnaniHttpTTSService( api_key="your-api-key", aiohttp_session=session, settings=GnaniHttpTTSService.Settings( voice="Pranav", ), ) ``` ### Text-to-Speech (SSE Streaming) ```python theme={null} from pipecat_gnani import GnaniSSETTSService tts = GnaniSSETTSService( api_key="your-api-key", aiohttp_session=session, settings=GnaniSSETTSService.Settings( voice="Pranav", ), ) ``` ### Text-to-Speech (WebSocket Streaming) ```python theme={null} from pipecat_gnani import GnaniTTSService tts = GnaniTTSService( api_key="your-api-key", settings=GnaniTTSService.Settings( voice="Pranav", ), ) ``` *** ## STT — `GnaniHttpSTTService` (REST) File-based transcription via REST. Extends Pipecat's `SegmentedSTTService`. * Calls `POST /stt/v3` * Requires VAD in the pipeline to segment audio * Requires an active `aiohttp.ClientSession` passed at construction time ```python theme={null} from pipecat_gnani import GnaniHttpSTTService from pipecat.transcriptions.language import Language stt = GnaniHttpSTTService( api_key="your-api-key", aiohttp_session=session, settings=GnaniHttpSTTService.Settings( language=Language.HI_IN, ), ) ``` ### Settings | Parameter | Type | Default | Description | | ---------- | ---------- | ---------------- | --------------------------------------------------------------------------------- | | `language` | `Language` | `Language.EN_IN` | Language enum for transcription. See [Supported Languages](#supported-languages). | *** ## STT — `GnaniSTTService` (WebSocket) Real-time streaming speech-to-text via WebSocket with VAD events. Extends Pipecat's `STTService`. * Connects to `wss://api.vachana.ai/stt/v3/stream` * Sends raw PCM audio in 1,024-byte frames * Emits `TranscriptionFrame` (final) and `InterimTranscriptionFrame` when the API sets `is_final: false` (today Gnani sends final transcripts only) * Supports 8 kHz and 16 kHz sample rates ```python theme={null} from pipecat_gnani import GnaniSTTService from pipecat.transcriptions.language import Language stt = GnaniSTTService( api_key="your-api-key", settings=GnaniSTTService.Settings( language=Language.HI_IN, sample_rate=16000, # 8000 or 16000 ), ) ``` ### Streaming PCM Specification All streaming audio must be sent as **raw PCM binary frames** — no container format (WAV, MP3) mid-stream. | Property | 16 kHz | 8 kHz | | ------------------- | --------------------------------------- | --------------------------------------- | | Encoding | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian | | Sample Rate | 16,000 Hz | 8,000 Hz | | Channels | 1 (mono) | 1 (mono) | | Samples per chunk | 512 | 512 | | **Bytes per frame** | **1,024 bytes** (512 samples × 2 bytes) | **1,024 bytes** (512 samples × 2 bytes) | | Frame duration | 32 ms | 64 ms | Frames must be sent at **real-time cadence**. See [STT Realtime — PCM Specification](/api/STT/stt-websocket#pcm-specification) for full details. ### Settings | Parameter | Type | Default | Description | | ------------- | ---------- | ---------------- | --------------------------------------------------------------------------------- | | `language` | `Language` | `Language.EN_IN` | Language enum for transcription. See [Supported Languages](#supported-languages). | | `sample_rate` | `int` | `16000` | Audio sample rate in Hz. Accepted values: `8000`, `16000`. | *** ## TTS — `GnaniHttpTTSService` (REST) REST-based text-to-speech for non-streaming use cases. Returns the complete audio in a single response. * Calls `POST /api/v1/tts/inference` * Requires an active `aiohttp.ClientSession` passed at construction time * Suitable for batch synthesis or pipelines where streaming is not needed ```python theme={null} import aiohttp from pipecat_gnani import GnaniHttpTTSService async def build_pipeline(): async with aiohttp.ClientSession() as session: tts = GnaniHttpTTSService( api_key="your-api-key", aiohttp_session=session, settings=GnaniHttpTTSService.Settings( voice="Pranav", ), ) ``` ### Settings | Parameter | Type | Default | Description | | ------------- | -------- | ---------- | ------------------------------------------------------------ | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `sample_rate` | `int` | `16000` | Output sample rate in Hz. | *** ## TTS — `GnaniSSETTSService` (SSE) Streaming text-to-speech via Server-Sent Events. Lower latency than REST. * Calls `POST /api/v1/tts/sse` * Requires an active `aiohttp.ClientSession` passed at construction time * Streams audio chunks as synthesis progresses ```python theme={null} from pipecat_gnani import GnaniSSETTSService tts = GnaniSSETTSService( api_key="your-api-key", aiohttp_session=session, settings=GnaniSSETTSService.Settings( voice="Pranav", ), ) ``` ### Settings | Parameter | Type | Default | Description | | ------------- | -------- | ---------- | ------------------------------------------------------------ | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `sample_rate` | `int` | `16000` | Output sample rate in Hz. | *** ## TTS — `GnaniTTSService` (WebSocket, recommended) Streaming text-to-speech via WebSocket. Extends Pipecat's `InterruptibleTTSService`, giving your agent built-in interruption (barge-in) support — when the user speaks over the agent, synthesis stops cleanly. * Connects to `wss://api.vachana.ai/api/v1/tts` * Streams audio chunks in real-time as synthesis progresses * Ideal for live conversational agents where latency and barge-in handling matter * `TTSTextFrame`s are emitted by the Pipecat base class after each synthesis request ```python theme={null} from pipecat_gnani import GnaniTTSService tts = GnaniTTSService( api_key="your-api-key", settings=GnaniTTSService.Settings( voice="Pranav", ), ) ``` ### Settings | Parameter | Type | Default | Description | | ------------- | -------- | ---------- | ------------------------------------------------------------ | | `voice` | `string` | `"Pranav"` | Voice ID. See [Available Voices](/api/TTS/available-voices). | | `sample_rate` | `int` | `16000` | Output sample rate in Hz. | *** ## Available Voices To see the available voices, [click here](/api/TTS/available-voices). *** ## Supported Languages ### STT Languages (Speech-to-Text) STT uses BCP-47 locale codes (e.g. `hi-IN`, `bn-IN`). For the full list, see [STT — Supported Languages](/api/STT/speech-to-text#supported-languages). ### TTS Languages (Text-to-Speech) TTS uses the same BCP-47 `-IN` suffixed codes as STT (e.g. `hi-IN`, `en-IN`, `ta-IN`). See [TTS REST — Request Parameters](/api/TTS/tts-inference#request-parameters) and the [Voice Catalog](/api/TTS/available-voices). Use the `Language` enum from `pipecat.transcriptions.language` for STT services (`GnaniSTTService`, `GnaniHttpSTTService`). TTS voice selection primarily drives language for synthesis. *** ## Further Reading * [STT REST API](/api/STT/speech-to-text) — file-based transcription reference * [STT Realtime API](/api/STT/stt-websocket) — WebSocket protocol reference * [TTS REST API](/api/TTS/tts-inference) — synchronous synthesis reference * [TTS Streaming (SSE)](/api/TTS/tts-sse) — SSE synthesis reference * [TTS Realtime API](/api/TTS/tts-websocket) — WebSocket TTS reference * [`gnani-vachana` on PyPI](https://pypi.org/project/gnani-vachana/) — core SDK * [`pipecat-gnani` on PyPI](https://pypi.org/project/pipecat-gnani/) — this plugin * [Pipecat Docs](https://docs.pipecat.ai/) — Pipecat framework reference # Python SDK Source: https://docs.gnani.ai/python-sdk/introduction Official Python client for Gnani Speech-to-Text and Text-to-Speech across REST, SSE, and WebSocket. ## Overview `gnani-vachana` is the official Python client for the Gnani Speech APIs. It wraps STT and TTS across all three transports — REST, SSE, and WebSocket — so you never hand-roll multipart uploads, SSE frame parsing, WAV headers, or WebSocket lifecycles. ```text theme={null} gnani-vachana ← pip install name └── gnani ← import name ├── gnani.stt → GnaniSTTClient (REST) · GnaniSTTStreamClient (WebSocket) └── gnani.tts → GnaniTTSClient (REST) · GnaniTTSStreamClient (SSE) GnaniTTSRealtimeClient (WebSocket) ``` The package is dependency-light — `requests` and `websockets` only — ships type hints (`py.typed`), and is maintained by [Gnani.ai](https://gnani.ai/). The PyPI package is `gnani-vachana`, but the Python import package is `gnani`. Install `gnani-vachana`, then `from gnani.stt import ...`. *** ## Installation ```bash theme={null} pip install gnani-vachana ``` Or with [uv](https://docs.astral.sh/uv/): ```bash theme={null} uv add gnani-vachana ``` **Requirements:** Python 3.9+ *** ## Authentication You need a Gnani API key — generate one on the [Gnani API platform](https://app.gnani.ai/voice). A single API key authenticates both STT and TTS; no `organization_id` or `user_id` is required. The recommended approach is the `GNANI_API_KEY` environment variable, which every client reads automatically: ```bash theme={null} export GNANI_API_KEY="your-api-key" ``` ```python theme={null} from gnani.stt import GnaniSTTClient client = GnaniSTTClient() # picks up GNANI_API_KEY ``` Or pass it explicitly: ```python theme={null} client = GnaniSTTClient(api_key="your-api-key") ``` If neither is present, the constructor raises `AuthenticationError` immediately — before any network call. If you are upgrading from an SDK version older than `0.7.x`, remove the `organization_id` and `user_id` constructor arguments. They are no longer accepted. *** ## Choosing a client Each API has one class per transport. Pick by latency requirement, not by preference — they return the same audio and the same transcripts. ### Speech-to-Text | Class | Transport | Use it for | | ---------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------- | | `GnaniSTTClient` | REST — `POST /stt/v3` | Pre-recorded files up to 60 s. Synchronous, one call, one transcript. | | `GnaniSTTStreamClient` | WebSocket — `/stt/v3/stream` | Live microphone or telephony audio. Server-side VAD emits a transcript per speech segment. `async`. | ### Text-to-Speech | Class | Transport | Use it for | | ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------- | | `GnaniTTSClient` | REST — `POST /api/v1/tts/inference` | Batch synthesis. Returns the complete audio in one response. | | `GnaniTTSStreamClient` | SSE — `POST /api/v1/tts/sse` | Progressive playback with lower time-to-first-audio than REST. Synchronous generator. | | `GnaniTTSRealtimeClient` | WebSocket — `/api/v1/tts` | Lowest latency, for conversational agents. `async`. | The two streaming STT/TTS clients are `async`; the REST and SSE clients are ordinary synchronous Python. You can mix them freely in the same program. *** ## Your first request ```python theme={null} from gnani.stt import GnaniSTTClient client = GnaniSTTClient() result = client.transcribe("recording.wav", language_code="hi-IN") print(result["transcript"]) ``` ```text theme={null} नमस्ते, आप कैसे हैं? ``` Continue to [Speech-to-Text →](/python-sdk/speech-to-text) ```python theme={null} from gnani.tts import GnaniTTSClient client = GnaniTTSClient() audio = client.synthesize( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", output_file="output.wav", ) ``` `output.wav` is written to disk, and the same bytes are returned so you can stream or upload them instead. Continue to [Text-to-Speech →](/python-sdk/text-to-speech) Always pass `model="timbre-v2.5"` on TTS calls. The SDK still defaults to `timbre-v2.0`, which the API no longer serves — omitting `model` returns `400` with `The supported models are "timbre-v2.5"`. *** ## Error handling Each sub-package defines its own exception hierarchy, rooted at `GnaniSTTError` and `GnaniTTSError`. Catching the root class catches everything the SDK raises. | Exception | Raised when | | ----------------------- | ------------------------------------------------------------------------ | | `AuthenticationError` | No API key was passed and `GNANI_API_KEY` is unset. | | `APIError` | The API returned a non-200 response. Carries `.status_code` and `.body`. | | `InvalidAudioError` | *(STT only)* The audio file is missing, or its extension is unsupported. | | `StreamConnectionError` | The WebSocket connection could not be established. | | `StreamClosedError` | An operation was attempted on a closed stream. | | `StreamError` | The server sent an error event mid-stream. | ```python theme={null} from gnani.stt import GnaniSTTClient from gnani.stt.exceptions import APIError, GnaniSTTError, InvalidAudioError client = GnaniSTTClient() try: result = client.transcribe("recording.wav", language_code="hi-IN") except InvalidAudioError as exc: print(f"Bad input: {exc}") except APIError as exc: print(f"API returned {exc.status_code}: {exc.body}") except GnaniSTTError as exc: print(f"SDK error: {exc}") ``` `ValueError` is raised — before any request leaves your process — for invalid parameter combinations such as an unknown voice, an unsupported sample rate, or `speed` outside `0.85`–`1.15`. *** ## Request IDs Every method accepts an optional `request_id`. When omitted, the SDK generates one and sends it as `X-API-Request-ID`. It is echoed back in STT REST responses and in TTS WebSocket events. ```python theme={null} result = client.transcribe( "recording.wav", language_code="hi-IN", request_id="order-4471-attempt-1", ) ``` Log it. It is the fastest way for Gnani support to trace a single call end to end. *** ## Pointing at a different environment All clients take `base_url`, defaulting to `https://api.vachana.ai`. The streaming clients derive their WebSocket URL from it automatically (`https` → `wss`). ```python theme={null} client = GnaniSTTClient(base_url="https://api.vachana.ai") ``` *** ## Further Reading REST transcription and realtime streaming with the Python SDK. REST, SSE, and WebSocket synthesis with the Python SDK. * [`gnani-vachana` on PyPI](https://pypi.org/project/gnani-vachana/) — releases and changelog * [STT REST API](/api/STT/speech-to-text) · [STT Realtime API](/api/STT/stt-websocket) — underlying HTTP reference * [TTS REST](/api/TTS/tts-inference) · [TTS SSE](/api/TTS/tts-sse) · [TTS Realtime](/api/TTS/tts-websocket) — underlying HTTP reference * [LiveKit Plugin](/livekit/introduction) · [Pipecat Plugin](/pipecat/introduction) — framework integrations built on these same APIs # Python SDK — Speech-to-Text Source: https://docs.gnani.ai/python-sdk/speech-to-text Transcribe files with the REST client and live audio with the realtime WebSocket client. ## Overview The SDK exposes two STT clients. Both authenticate with a single API key and support the same ten Indian languages. | Class | Transport | Audio source | Returns | | ---------------------- | ---------------------------- | ----------------------------- | ----------------------------------------------------- | | `GnaniSTTClient` | REST — `POST /stt/v3` | A file or `bytes`, up to 60 s | One transcript, synchronously | | `GnaniSTTStreamClient` | WebSocket — `/stt/v3/stream` | A live PCM stream | A transcript per speech segment, as they are detected | ```python theme={null} from gnani.stt import GnaniSTTClient, GnaniSTTStreamClient ``` *** ## REST — transcribe a file Pass a path, a `Path`, or any binary file object. Paths are opened and closed for you. ```python theme={null} from gnani.stt import GnaniSTTClient client = GnaniSTTClient() # or GnaniSTTClient(api_key="your-api-key") result = client.transcribe("recording.wav", language_code="hi-IN") print(result["transcript"]) print(result["request_id"]) ``` ### Response `transcribe()` returns the parsed JSON body as a `dict`: ```json theme={null} { "success": true, "request_id": "019fd17d-1106-7265-88ab-3ed12d029292", "timestamp": "20260917_143052.123", "transcript": "नमस्ते, आप कैसे हैं?" } ``` ### Parameters | Parameter | Type | Default | Description | | --------------------- | ------------------------- | ------------ | ------------------------------------------------------------------ | | `audio` | `str \| Path \| BinaryIO` | *required* | Path to an audio file, or an open binary file object. | | `language_code` | `str` | `"en-IN"` | BCP-47 code. See [Supported languages](#supported-languages). | | `format` | `str` | `"verbatim"` | `"verbatim"` for spoken-form output, `"transcribe"` to enable ITN. | | `itn_native_numerals` | `bool` | `False` | With `format="transcribe"`, render digits in the native script. | | `request_id` | `str` | auto | Custom ID for tracing. | Accepted file extensions: `.wav`, `.mp3`, `.flac`, `.ogg`, `.m4a`, `.aac`. Anything else raises `InvalidAudioError` locally, without a network round trip. ### Inverse Text Normalization `format="transcribe"` rewrites numbers, currency, dates, and phone numbers into conventional written form. Add `itn_native_numerals=True` to keep digits in the target script. ```python Verbatim theme={null} result = client.transcribe("invoice.wav", language_code="hi-IN") # "पाँच हज़ार रुपये" ``` ```python ITN theme={null} result = client.transcribe( "invoice.wav", language_code="hi-IN", format="transcribe", ) # "₹5,000" ``` ```python ITN + native numerals theme={null} result = client.transcribe( "invoice.wav", language_code="hi-IN", format="transcribe", itn_native_numerals=True, ) # "₹५,०००" ``` ITN is currently supported for `hi-IN` and `en-IN` only. See [ITN reference](/api/STT/speech-to-text#inverse-text-normalization-itn). *** ## REST — transcribe raw bytes When audio never touches disk — an upload handler, an S3 object, a recording buffer — use `transcribe_bytes()`. The `filename` is a format hint for the server, not a real path. ```python theme={null} audio_bytes = request.files["audio"].read() result = client.transcribe_bytes( audio_bytes, filename="upload.wav", language_code="en-IN", ) print(result["transcript"]) ``` ### Parameters | Parameter | Type | Default | Description | | --------------------- | ------- | ------------- | ------------------------------------------------ | | `audio_bytes` | `bytes` | *required* | Raw audio content. | | `filename` | `str` | `"audio.wav"` | Extension tells the server which decoder to use. | | `language_code` | `str` | `"en-IN"` | BCP-47 code. | | `format` | `str` | `"verbatim"` | `"verbatim"` or `"transcribe"`. | | `itn_native_numerals` | `bool` | `False` | Native-script digits when ITN is on. | | `request_id` | `str` | auto | Custom ID for tracing. | *** ## Realtime — streaming WebSocket `GnaniSTTStreamClient` streams raw PCM to the server. Server-side VAD detects end of speech and emits a transcript per segment — you do not need to decide where utterances end. The client is `async` and works as an async context manager: entering connects, exiting closes. ```python theme={null} import asyncio from gnani.stt import GnaniSTTStreamClient, StreamTranscriptEvent async def main(): async with GnaniSTTStreamClient(language_code="hi-IN", sample_rate=16000) as stream: with open("audio.pcm", "rb") as f: while chunk := f.read(1024): await stream.send_audio(chunk) await asyncio.sleep(0.032) # real-time pacing, 32 ms per frame async for event in stream: if isinstance(event, StreamTranscriptEvent): print(event.text) asyncio.run(main()) ``` ### Constructor parameters | Parameter | Type | Default | Description | | --------------------- | ------ | -------------------------- | ----------------------------------------------------------------------- | | `api_key` | `str` | `GNANI_API_KEY` | Your API key. | | `language_code` | `str` | `"en-IN"` | BCP-47 code. Streaming is **single-language** — one locale per session. | | `sample_rate` | `int` | `16000` | `8000`, `16000`, `44100`, or `48000`. | | `format` | `str` | `"verbatim"` | `"verbatim"` or `"transcribe"`. | | `itn_native_numerals` | `bool` | `False` | Native-script digits when ITN is on. | | `base_url` | `str` | `"https://api.vachana.ai"` | Converted to `wss://` automatically. | Invalid values raise `ValueError` in the constructor, before connecting. ### PCM specification Audio must be **raw PCM binary frames** — no WAV, MP3, or other container mid-stream. | Property | 16 kHz | 8 kHz | | ------------------- | ------------------------------- | ------------------------------- | | Encoding | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian | | Channels | 1 (mono) | 1 (mono) | | Samples per frame | 512 | 512 | | **Bytes per frame** | **1,024** | **1,024** | | Frame duration | 32 ms | 64 ms | The SDK exports these as constants so you never hard-code them: ```python theme={null} from gnani.stt import STREAM_CHUNK_BYTES, STREAM_CHUNK_SAMPLES STREAM_CHUNK_SAMPLES # 512 STREAM_CHUNK_BYTES # 1024 ``` Send frames at real-time cadence. Flushing a whole file as fast as the socket accepts it starves the VAD of timing information and degrades segmentation. `asyncio.sleep(bytes / 2 / sample_rate)` is the correct pause between frames — or use `stream_audio()` below, which paces for you. *** ## Realtime — three usage patterns `stream_audio()` sends and receives concurrently, paces frames for you, and returns every transcript when the source is exhausted. Best for transcribing a file or a finite stream. ```python theme={null} import asyncio from gnani.stt import GnaniSTTStreamClient async def main(): async with GnaniSTTStreamClient(language_code="hi-IN") as stream: with open("audio.pcm", "rb") as f: transcripts = await stream.stream_audio( f, on_transcript=lambda e: print(f"[{e.segment_index}] {e.text}"), on_error=lambda e: print(f"error: {e.message}"), ) full_text = " ".join(t.text for t in transcripts) print(full_text) asyncio.run(main()) ``` | Parameter | Type | Default | Description | | --------------- | ---------------------------------- | ---------- | --------------------------------------------------------------------------- | | `audio_source` | `AsyncIterator[bytes] \| BinaryIO` | *required* | Async iterator of chunks, or a binary file object. | | `on_transcript` | `callable` | `None` | Called per `StreamTranscriptEvent`. | | `on_processing` | `callable` | `None` | Called per `StreamProcessingEvent`. | | `on_error` | `callable` | `None` | Called per `StreamErrorEvent`. If unset, a `StreamError` is raised instead. | | `chunk_size` | `int` | `1024` | Bytes per frame. | | `realtime_pace` | `bool` | `True` | Sleep between frames to match real time. | Iterate the client directly to handle every event type as it arrives. Best for live microphone or telephony audio, where sending never ends. ```python theme={null} import asyncio from gnani.stt import ( GnaniSTTStreamClient, StreamConnectedEvent, StreamErrorEvent, StreamProcessingEvent, StreamTranscriptEvent, ) async def consume(stream): async for event in stream: if isinstance(event, StreamConnectedEvent): print(f"connected at {event.sample_rate} Hz") elif isinstance(event, StreamProcessingEvent): print("end of speech — transcribing…") elif isinstance(event, StreamTranscriptEvent): print(f"{event.text} ({event.latency} ms)") elif isinstance(event, StreamErrorEvent): print(f"error: {event.message}") async def main(): async with GnaniSTTStreamClient(language_code="en-IN") as stream: consumer = asyncio.create_task(consume(stream)) async for chunk in microphone_frames(): # your audio source await stream.send_audio(chunk) await stream.close() await consumer asyncio.run(main()) ``` Run sending and receiving as separate tasks. Awaiting them in sequence on an unbounded source means transcripts are never read. Call `connect()` and `close()` yourself when the connection outlives a single block — for example, a stream owned by a long-lived agent object. ```python theme={null} stream = GnaniSTTStreamClient(language_code="ta-IN", sample_rate=8000) connected = await stream.connect() print(connected.sample_rate, connected.chunk_size) await stream.send_audio(frame) # repeat per 1,024-byte frame transcripts = await stream.close() # returns every transcript from the session ``` | Member | Description | | -------------------------------- | -------------------------------------------------------------------- | | `await connect(request_id=None)` | Opens the socket, returns the `StreamConnectedEvent`. | | `await send_audio(chunk)` | Sends one binary frame. Raises `StreamClosedError` if not connected. | | `await close()` | Closes gracefully, returns `list[StreamTranscriptEvent]`. | | `is_connected` | `bool` — whether the socket is open. | | `connected_config` | The `StreamConnectedEvent`, or `None` before connecting. | | `transcripts` | Every transcript received so far this session. | *** ## Stream events Every event is a typed dataclass. Each carries `.raw`, the untouched JSON payload, for anything not surfaced as an attribute. Received once, immediately after the handshake. Human-readable status from the server. ISO-8601 timestamp. Negotiated sample rate in Hz. Expected frame size in samples. VAD has detected end of speech and transcription has begun. Useful for a "thinking" indicator. ISO-8601 timestamp. The transcript for one completed speech segment. The transcribed text. Duration of the segment in milliseconds. Unique identifier for the segment. Ordinal index within the session. Server-side processing latency in milliseconds. ISO-8601 timestamp. The server encountered an error. The connection may still be usable. Error description. ISO-8601 timestamp. *** ## Supported languages Both clients support the same ten languages. STT uses the `-IN` suffix throughout. | Code | Language | Code | Language | | ------- | --------------- | ------- | --------- | | `en-IN` | English (India) | `mr-IN` | Marathi | | `hi-IN` | Hindi | `bn-IN` | Bengali | | `gu-IN` | Gujarati | `ml-IN` | Malayalam | | `ta-IN` | Tamil | `pa-IN` | Punjabi | | `kn-IN` | Kannada | `te-IN` | Telugu | Read them at runtime instead of hard-coding: ```python theme={null} from gnani.stt import GnaniSTTClient, GnaniSTTStreamClient GnaniSTTClient.supported_languages() # REST GnaniSTTStreamClient.supported_languages() # realtime # {"en-IN": "English (India)", "hi-IN": "Hindi", ...} ``` Realtime streaming is single-language — one `language_code` per session. Multi-language input is supported only on REST `POST /stt/v3` via `multi_lang_codes`, which is not yet exposed through the SDK. *** ## Error handling ```python theme={null} from gnani.stt import GnaniSTTStreamClient from gnani.stt.exceptions import ( APIError, AuthenticationError, InvalidAudioError, StreamClosedError, StreamConnectionError, StreamError, ) ``` | Exception | Raised when | | ----------------------- | ----------------------------------------------------------------------- | | `AuthenticationError` | No API key passed and `GNANI_API_KEY` unset. | | `InvalidAudioError` | File missing, or extension unsupported. | | `APIError` | Non-200 REST response. Carries `.status_code` and `.body`. | | `StreamConnectionError` | The WebSocket handshake failed. | | `StreamClosedError` | `send_audio()` called on a closed stream. | | `StreamError` | The server sent an error event and no `on_error` callback was provided. | All six inherit from `GnaniSTTError`. *** ## Further Reading * [Python SDK overview](/python-sdk/introduction) — installation, auth, client selection * [Python SDK — Text-to-Speech](/python-sdk/text-to-speech) * [STT REST API](/api/STT/speech-to-text) — endpoint reference * [STT Realtime API](/api/STT/stt-websocket) — WebSocket protocol reference # Python SDK — Text-to-Speech Source: https://docs.gnani.ai/python-sdk/text-to-speech Synthesize speech with the REST, SSE, and realtime WebSocket clients. ## Overview The SDK exposes three TTS clients — one per transport. They accept identical arguments and produce identical audio; they differ only in how quickly the first bytes reach you. | Class | Transport | Style | Use it for | | ------------------------ | --------- | -------------- | ----------------------------------------------------- | | `GnaniTTSClient` | REST | sync | Batch jobs, short strings, anything written to a file | | `GnaniTTSStreamClient` | SSE | sync generator | Progressive playback without `asyncio` | | `GnaniTTSRealtimeClient` | WebSocket | `async` | Conversational agents, lowest time-to-first-audio | ```python theme={null} from gnani.tts import GnaniTTSClient, GnaniTTSStreamClient, GnaniTTSRealtimeClient ``` Always pass `model="timbre-v2.5"`. The SDK's built-in default is still `timbre-v2.0`, which the API no longer serves — a call without an explicit `model` fails with `400` and `The supported models are "timbre-v2.5"`. Every example on this page sets it explicitly. *** ## REST — single response Returns the complete audio as `bytes`. Pass `output_file` to also write it to disk; parent directories are created for you. ```python theme={null} from gnani.tts import GnaniTTSClient client = GnaniTTSClient() # or GnaniTTSClient(api_key="your-api-key") audio = client.synthesize( "नमस्ते, आप कैसे हैं?", voice="Nalini", model="timbre-v2.5", language="hi-IN", output_file="output.wav", ) print(len(audio), "bytes") ``` Without `output_file`, handle the bytes yourself — upload them, return them from a web handler, or push them into an audio device. ```python theme={null} audio = client.synthesize("Hello there", voice="Pranav", model="timbre-v2.5", language="en-IN") return Response(audio, mimetype="audio/wav") ``` ### Parameters Shared by all three clients unless noted. | Parameter | Type | Default | Description | | ------------------- | ------------------ | --------------- | ----------------------------------------------------------------------------------------------------- | | `text` | `str` | *required* | The text to synthesize. | | `voice` | `str` | `"Pranav"` | Voice ID. See [Voices](#voices). Ignored when `speaker_embedding` is given. | | `model` | `str` | `"timbre-v2.0"` | **Set to `"timbre-v2.5"`.** | | `language` | `str` | `None` | BCP-47 code. `timbre-v2.5` only. | | `speed` | `float` | `1.0` | Speaking rate, `0.85`–`1.15`. `timbre-v2.5` only. | | `audio_config` | `AudioConfig` | 48 kHz WAV | Output format. See [Audio configuration](#audio-configuration). | | `speaker_embedding` | `SpeakerEmbedding` | `None` | Cloned voice. See [Voice cloning](#voice-cloning). | | `output_file` | `str \| Path` | `None` | Write the audio here. Not available on `synthesize_stream()` / `synthesize()` on the realtime client. | | `request_id` | `str` | auto | Custom ID for tracing. | Invalid combinations raise `ValueError` locally — an unknown voice for the model, `speed` out of range, or `language`/`speed` passed with a non-`timbre-v2.5` model. *** ## SSE — progressive playback `synthesize_stream()` yields raw PCM chunks as they are generated, so playback can start before synthesis finishes. Chunks carry **no WAV header**. ```python theme={null} from gnani.tts import GnaniTTSStreamClient client = GnaniTTSStreamClient() for chunk in client.synthesize_stream( "Your order has been confirmed and will arrive tomorrow.", voice="Kaveri", model="timbre-v2.5", language="en-IN", ): player.write(chunk) # feed your audio device as bytes arrive ``` If you want a finished file instead, `synthesize()` collects every chunk and adds a RIFF header when the output is linear PCM WAV: ```python theme={null} audio = client.synthesize( "Your order has been confirmed.", voice="Kaveri", model="timbre-v2.5", language="en-IN", output_file="confirmation.wav", ) ``` The header is added only for `encoding="linear_pcm"` with `container="wav"`. For `mp3`, `oggopus`, or telephony encodings, the server's bytes are returned untouched — those formats carry their own framing. *** ## Realtime WebSocket — lowest latency `GnaniTTSRealtimeClient` is `async` and offers three shapes, depending on how much control you need. `synthesize()` is an async generator of audio bytes. Use it when audio should start playing immediately. ```python theme={null} import asyncio from gnani.tts import GnaniTTSRealtimeClient async def main(): async with GnaniTTSRealtimeClient() as client: async for chunk in client.synthesize( "नमस्ते, मैं आपकी कैसे मदद कर सकता हूँ?", voice="Nalini", model="timbre-v2.5", language="hi-IN", ): await player.feed(chunk) asyncio.run(main()) ``` `synthesize_and_collect()` waits for the full response and returns it, with a WAV header when the output is linear PCM WAV. ```python theme={null} import asyncio from gnani.tts import GnaniTTSRealtimeClient async def main(): async with GnaniTTSRealtimeClient() as client: audio = await client.synthesize_and_collect( "नमस्ते, मैं आपकी कैसे मदद कर सकता हूँ?", voice="Nalini", model="timbre-v2.5", language="hi-IN", output_file="reply.wav", ) asyncio.run(main()) ``` `synthesize_events()` yields typed events instead of bare bytes, so you can read `chunk_index`, detect the final chunk, and log the server's `request_id`. ```python theme={null} import asyncio from gnani.tts import ( GnaniTTSRealtimeClient, TTSAudioChunkEvent, TTSCompletedEvent, TTSStartEvent, ) async def main(): async with GnaniTTSRealtimeClient() as client: async for event in client.synthesize_events( "Your order has been confirmed.", voice="Pranav", model="timbre-v2.5", language="en-IN", ): if isinstance(event, TTSStartEvent): print(f"started — {event.request_id}") elif isinstance(event, TTSAudioChunkEvent): await player.feed(event.data) if event.is_final: print("last audio chunk") elif isinstance(event, TTSCompletedEvent): print(f"done — {event.total_chunks} chunks") asyncio.run(main()) ``` ### Events The server has begun streaming audio. Identifier for this TTS request. Status message from the server. One binary audio chunk. Raw audio bytes for this chunk. Zero-based index within the response. `True` on the last audio-bearing chunk. Synthesis is finished and the connection is closing. Identifier for this TTS request. Number of audio chunks sent. Each call to `synthesize()`, `synthesize_events()`, or `synthesize_and_collect()` opens its own WebSocket connection and closes it when the server finishes. The `async with` block is for symmetry and future-proofing — it does not pool connections, so reusing one client instance across many calls is safe and cheap. *** ## Audio configuration `AudioConfig` controls the output format. The default is 48 kHz, 16-bit mono WAV. ```python theme={null} from gnani.tts import AudioConfig, GnaniTTSClient client = GnaniTTSClient() audio = client.synthesize( "Your order has been confirmed.", voice="Pranav", model="timbre-v2.5", language="en-IN", audio_config=AudioConfig(sample_rate=48000, container="mp3", bitrate="128k"), output_file="confirmation.mp3", ) ``` | Field | Type | Default | Accepted values | | -------------- | ----- | -------------- | --------------------------------------------------- | | `sample_rate` | `int` | `48000` | `8000`, `16000`, `22050`, `24000`, `44100`, `48000` | | `encoding` | `str` | `"linear_pcm"` | `linear_pcm`, `oggopus`, `pcm_mulaw`, `pcm_alaw` | | `num_channels` | `int` | `1` | `1`–`8` | | `sample_width` | `int` | `2` | `1`–`4` (bytes per sample) | | `container` | `str` | `"wav"` | `raw`, `wav`, `mp3`, `ogg`, `mulaw`, `alaw` | | `bitrate` | `str` | `None` | `32k`, `64k`, `96k`, `128k`, `192k` — MP3 only | ### Common presets ```python Web playback (WAV) theme={null} AudioConfig(sample_rate=48000, encoding="linear_pcm", container="wav") ``` ```python Compressed download (MP3) theme={null} AudioConfig(sample_rate=48000, container="mp3", bitrate="128k") ``` ```python Telephony (µ-law, 8 kHz) theme={null} AudioConfig(sample_rate=8000, encoding="pcm_mulaw", container="mulaw") ``` ```python Telephony (A-law, 8 kHz) theme={null} AudioConfig(sample_rate=8000, encoding="pcm_alaw", container="alaw") ``` ```python Opus theme={null} AudioConfig(encoding="oggopus", container="raw") ``` Unsupported values raise `ValueError` before the request is sent. *** ## Voices `timbre-v2.5` offers 42 voices. Each has a preferred language — match `voice` to `language` for best quality. | Language | `language` | Voices | | :-------- | :--------- | :---------------------------------------------------------------------------------------------------- | | English | `en-IN` | Kaveri, Trupti, Devika, Pranav, Shlok, Girish | | Hindi | `hi-IN` | Nalini, Bhavna, Yashvi, Urmila, Jwala, Chitra, Ambuja, Deepak, Roopesh, Vikrant, Hemraj, Jalaj, Omkar | | Tamil | `ta-IN` | Asmita, Trisha, Brinda, Vedika, Noopur | | Telugu | `te-IN` | Suhana, Lehara, Lavanya, Yukti, Varuni | | Kannada | `kn-IN` | Saanvi, Kavin | | Malayalam | `ml-IN` | Reshma, Riyaan | | Marathi | `mr-IN` | Zahira, Ishaan | | Bengali | `bn-IN` | Kirra, Dhruva | | Gujarati | `gu-IN` | Falak, Veera | | Punjabi | `pa-IN` | Mehuli, Zayan | | Hinglish | `auto` | Poorvi | Query the list at runtime rather than hard-coding it: ```python theme={null} from gnani.tts import GnaniTTSClient GnaniTTSClient.supported_voices("timbre-v2.5") # ["Ambuja", "Asmita", "Bhavna", ...] ``` Preview voices before integrating — open the [Gnani TTS playground](https://app.gnani.ai/voice/text-to-speech) and filter by language, gender, and persona. Full descriptions are in the [voice catalog](/api/TTS/available-voices). ### Language and speed Both are `timbre-v2.5`-only. Passing either with another model raises `ValueError`. ```python theme={null} audio = client.synthesize( "आपका ऑर्डर कन्फर्म हो गया है।", voice="Nalini", model="timbre-v2.5", language="hi-IN", speed=1.1, # 0.85 – 1.15 ) ``` Accepted `language` values: `auto`, `hi-IN`, `en-IN`, `ta-IN`, `te-IN`, `kn-IN`, `ml-IN`, `mr-IN`, `bn-IN`, `gu-IN`, `pa-IN`. Use `auto` to detect the language from the input script — this is also what the Hinglish voice expects. *** ## Voice cloning Generate a speaker embedding with the [voice-clone embeddings endpoint](/api/VC/voice-clone-embeddings), then pass it as `SpeakerEmbedding`. When present, `voice` is ignored. ```python theme={null} from gnani.tts import GnaniTTSClient, SpeakerEmbedding embedding = SpeakerEmbedding( embedding=embedding_string, # from POST /api/v1/tts/voice-clone/embeddings shape=[1, 768], dtype="torch.bfloat16", ) audio = client.synthesize( "This is my cloned voice.", model="timbre-v2.5", speaker_embedding=embedding, output_file="cloned.wav", ) ``` `SpeakerEmbedding` works identically on all three clients. *** ## Error handling ```python theme={null} from gnani.tts import GnaniTTSClient from gnani.tts.exceptions import APIError, GnaniTTSError client = GnaniTTSClient() try: audio = client.synthesize( "Hello there", voice="Pranav", model="timbre-v2.5", language="en-IN", ) except APIError as exc: print(f"API returned {exc.status_code}: {exc.body}") except GnaniTTSError as exc: print(f"SDK error: {exc}") except ValueError as exc: print(f"Invalid parameters: {exc}") ``` | Exception | Raised when | | ----------------------- | ----------------------------------------------------------------------------------------------------- | | `AuthenticationError` | No API key passed and `GNANI_API_KEY` unset. | | `APIError` | Non-200 response. Carries `.status_code` and `.body`. | | `StreamConnectionError` | The WebSocket handshake failed. | | `StreamClosedError` | An operation was attempted on a closed stream. | | `StreamError` | The server sent an error event mid-stream. | | `ValueError` | Invalid voice, model, sample rate, container, bitrate, or speed — raised locally, before any request. | All except `ValueError` inherit from `GnaniTTSError`. *** ## Further Reading * [Python SDK overview](/python-sdk/introduction) — installation, auth, client selection * [Python SDK — Speech-to-Text](/python-sdk/speech-to-text) * [Available Voices](/api/TTS/available-voices) — full catalog with personas * [TTS REST](/api/TTS/tts-inference) · [TTS SSE](/api/TTS/tts-sse) · [TTS Realtime](/api/TTS/tts-websocket) — endpoint references * [Text Formatting](/api/TTS/tts-input-formating) — pauses, numbers, and pronunciation control