Vanira Documentation Audit
The docs are a single-page SPA with surprisingly deep API surface coverage but a string of copy-paste hazards: every Create/Update cURL example double-emits its body (nested object + flat dot-keys), the dedicated /api-docs page is an empty SPA shell, the official status page does not resolve, and internal-implementation brands (VoBiz, Hasura, Asterisk) leak into user-facing prose without ever being defined.
1. Every Create/Update cURL example double-emits the body as both nested object and flat dot-keys (critical)
Location: /docs#agents (Create Agent, Update Agent), /docs#orchestration (Create Call)
Problem: The Create Agent example sends a structured object AND duplicate flat dot-keys for the same fields:
"model": { "provider": "AZURE", "model": "gpt-4.1-mini" },
"model.provider": "AZURE",
"model.model": "gpt-4.1-mini",
"voice": { "provider": "ELEVENLABS", "name": "Riya Rao" },
"voice.provider": "ELEVENLABS",
"voice.name": "Riya Rao",
The Create Call example does the same thing with sip_data / sip_data.to / sip_data.name / sip_data.from_number. The intro paragraph on Create Agent explicitly says "Pass model, voice, and transcription as structured objects — no internal UUIDs needed."
Consequence: This is a documentation generator bug, not a schema. Any developer (or AI coding agent) copy-pasting the snippet sends a payload that either (a) is rejected as VALIDATION_ERROR because dot-keys aren't a documented field shape, or (b) is silently coerced server-side in undefined ways. The prose contradicts the example on the same page. Agents have no way to know which form is canonical.
The fix: Fix the template renderer so it stops flattening nested objects into sibling dot-keys. Show only one form — the nested object the prose advertises. Audit every cURL block in the docs for the same issue (it appears wherever a body has a nested object).
2. The dedicated API reference page (/api-docs) is an empty SPA shell (critical)
Location: https://vanira.io/api-docs
Problem: The page that the llms-full.txt index labels as the "API Reference" loads with 0 characters of body content — only the homepage <title> ("Vanira | True Agentic Voice AI from India") is returned. There is no machine-readable OpenAPI/Swagger spec linked anywhere in the docs or sitemap. The sitemap.xml only lists 8 URLs (homepage, blog, /docs, /research, 4 blog posts) — no /openapi.json, no spec file, no per-endpoint pages.
Consequence: AI coding agents cannot enumerate endpoints programmatically — they have to scrape the JS-rendered single /docs page. Human developers following a "View API Reference" link land on a blank page. There is no spec to import into Postman, Insomnia, or generate clients from.
The fix: Either (a) repair /api-docs to render the reference, or (b) publish an OpenAPI spec at a stable URL (/openapi.json or /api-docs/openapi.json) and link to it from the docs nav and llms.txt. Add the spec URL to sitemap.xml.
3. Status page hostname does not resolve (critical)
Location: https://status.vanira.io (referenced from llms-full.txt "Resources & Support")
Problem: https://status.vanira.io fails with ECONNREFUSED. The hostname is referenced as the canonical "Status Page" in the llms-full.txt that Vanira itself publishes.
Consequence: During an outage there is no incident channel for customers. Anyone integrating against api.vanira.io who needs to verify "is this me or them?" has nowhere to check. For an enterprise-positioned "sovereign" voice platform with sub-500ms latency claims, a missing status page is a procurement red flag.
The fix: Either stand up a real status page at that URL (Statuspage, Instatus, BetterStack, etc.) or remove the reference from llms-full.txt and replace it with whatever actually exists.
4. Installation section has no install command (critical)
Location: /docs#getting-started → SDK → Installation
Problem: The Installation section's prose says "Install the official Vanira AI SDK via npm or yarn to integrate real-time voice AI into your web application." That is the entire section. There is no npm install @vanira/sdk, no yarn add command, no package name confirmation. The llms-full.txt advertises the package as @vanira/sdk but the rendered docs never show that string in the Installation block.
Consequence: A developer following the SDK quickstart cannot start. They have to guess the package name from the llms-full.txt header line or the dashboard. Agents extracting "how do I install this" produce nothing usable.
The fix: Drop a code block with both npm install @vanira/sdk and yarn add @vanira/sdk (or whichever package name is canonical), plus a minimum Node/browser-target version note.
5. End Call built-in tool's implementation example uses a literal string as a URL (critical)
Location: /docs#tools → End Call
Problem: The "IMPLEMENTATION EXAMPLE" for the End Call built-in tool is:
const response = await fetch('end_call', { ... });
'end_call' is the tool's ref_code, not a URL. In the browser this resolves to <current-origin>/end_call and 404s.
Consequence: Copy-paste from this block hits the developer's own origin, not Vanira. Agents indexing the example will treat end_call as an API path. Worse, "End Call" is described as auto-attached to every agent — meaning the developer shouldn't be calling any URL at all; the SDK fires it. The example contradicts its own framing.
The fix: Remove the fetch() snippet entirely (the prose already says End Call is auto-attached and doesn't need invocation), or replace it with an SDK call such as client.invokeTool('end_call') if that is the intended pattern.
6. Built-in tool endpoints all share the generic POST /tools URL with empty cURL bodies (critical)
Location: /docs#tools → Human Transfer, Agent Transfer
Problem: Both "Human Transfer" and "Agent Transfer" advertise themselves under POST https://api.vanira.io/tools (the generic create-tool endpoint) with cURL stubs containing no body:
curl -X POST 'https://api.vanira.io/tools' \
-H "Content-Type: application/json"
No payload shape, no parameter list, no example of what distinguishes a Human Transfer create call from an Agent Transfer create call from a generic tool create call.
Consequence: A developer trying to wire up call escalation has nothing to send. The endpoint will reject with MISSING_FIELD or similar, and the docs offer zero guidance on what fields are required. AI agents will hallucinate the body shape.
The fix: Either show the full create payload for each built-in (with ref_code, type, execution_mode, the destination parameters) or — preferably — document built-ins as preconfigured rather than something you create via POST /tools, and show the attach-to-agent flow instead.
7. Create Data Collection example combines data_type: "boolean" with enum_values: ["Low","Medium","High"] (critical)
Location: /docs#agents → Create Data Collection
Problem: The body parameter description says enum_values is "List of valid values if data_type is enum." The implementation example then sends:
"data_type": "boolean",
"enum_values": ["Low", "Medium", "High"]
A boolean field with three enum values is internally contradictory. The key/prompt ("Did the user express interest in a demo? Answer yes or no.") confirms boolean is the intended type — so the enum_values array is junk template fill.
Consequence: Copy-paste either fails validation or, worse, silently strips enum_values and confuses developers about how enums actually work. AI agents will replicate the malformed pattern in new data-collection setups.
The fix: Split the example into two: one boolean (no enum_values), one enum (data_type: "enum" + enum_values: ["Low","Medium","High"]). Stop using placeholder strings like "<prospect_column_name>" together with store_to_prospect: false — they're contradictory.
8. Data-collection error codes all say AGENT_NOT_FOUND when the missing resource is a parameter (significant)
Location: /docs#agents → Update Data Collection, Delete Data Collection
Problem: Both endpoints document 404 AGENT_NOT_FOUND — Parameter not found. The error code references the wrong resource — these endpoints operate on :collection_id, not :agent_id.
Consequence: A developer who hits a 404 cannot tell whether their parent agent was deleted, their data-collection record was deleted, or they got the IDs mixed up. Error-handling code that branches on code === "AGENT_NOT_FOUND" will route the wrong way. If this is the literal string the API returns, it's also an API bug. If it's a docs typo, it misleads error-handling logic.
The fix: If the API actually returns AGENT_NOT_FOUND, change it to DATA_COLLECTION_NOT_FOUND and version the change. If it's a docs typo, fix the docs.
9. List Numbers documents 400 BAD_REQUEST — Missing client_id but no client_id parameter (significant)
Location: /docs#telephony → List Numbers
Problem: GET /phone-numbers documents the error 400 BAD_REQUEST — Missing client_id. There is no client_id listed in this endpoint's path or query parameters; the prose says it "Returns all active phone numbers connected to your account." The List VoBiz Credentials endpoint a few sections down DOES require client_id as a query parameter — looks like the error table was copy-pasted from that endpoint.
Consequence: Either (a) the endpoint really does require client_id and the parameter table is wrong (in which case every documented request fails with 400), or (b) the error code is bogus. Developers will spend time A/B-testing which is which.
The fix: Decide which is true. If client_id is needed, add it to the QUERY PARAMETERS list with REQUIRED UUID. If not, remove the 400 row.
10. Delete Number example uses double URL-encoded %252B (significant)
Location: /docs#telephony → Delete Number
Problem: The cURL example is:
curl -X DELETE 'https://api.vanira.io/phone-numbers/%252B918071387205'
%252B is + URL-encoded twice (+ → %2B → %252B). On the server this decodes to the literal string %2B918071387205, not +918071387205.
Consequence: A developer who runs the example as-is targets a number that doesn't exist and gets a 404 — or, worse, in a buggy server that decodes once, hits the wrong record. Copy-paste reliability for path-encoded E.164 numbers is exactly the thing the example needs to demonstrate correctly.
The fix: Use a single-encoded %2B918071387205 and add a one-line note explaining that the leading + must be percent-encoded once.
11. Provider naming triangle: vonage / twilio / vobiz (significant)
Location: /docs#campaigns
Problem: Create Campaign documents the provider field as REQUIRED STRING — The voice provider to use (e.g., vonage, twilio). The List Campaigns response example then returns "provider": "vobiz". The Telephony section uses VoBiz throughout. Three different vendor names appear in the same docs section with no enumeration of accepted values.
Consequence: Developers don't know what to send. If they send "vonage" they may get a 400. If they send "vobiz" based on the response example, they might or might not be authorized. AI agents see contradictory enums and produce broken code.
The fix: Enumerate the accepted provider values explicitly (vobiz | vonage | twilio | ...) and make the create example and the list example agree.
12. JWT auth referenced but never documented (significant)
Location: /docs#outbound-calling → List Calls, Get Call Details
Problem: Auth blocks state: "Requires X-API-Key: sk_live_* or Authorization: Bearer <token>." JWTs / bearer tokens are mentioned nowhere else — not in the API Keys overview (which describes only sk_live_* and pk_live_*), not in any sign-in flow, not in any token-issuance endpoint. The 500 HASURA_SERVICE_ERROR on Create Agent and the migrated-from-n8n notes suggest this is dashboard-session JWT leaking into public docs.
Consequence: A developer who tries to use Authorization: Bearer … has no way to obtain a token. If they're not supposed to use it, mentioning it as an equal alternative is misleading. Agents will hallucinate a "get token" flow that doesn't exist.
The fix: Either document the JWT issuance flow end-to-end (where do you get the token, what's its lifetime, how do you refresh) or remove the Authorization: Bearer references and stick to X-API-Key for the public API.
13. Internal-implementation names leak into user-facing docs (significant)
Location: /docs#telephony, /docs#campaigns, /docs#agents
Problem: User-facing prose and error messages reference third-party/internal names that are never defined:
Hasura—500 HASURA_SERVICE_ERROR — Failed to persist the new agent.Asterisk— response fieldasterisk_reloadedVoBiz— entire credential subsystem, errors like502 BAD_GATEWAY — Upstream failure (VoBiz or Hasura unreachable).vobiz-1387205(trunk_endpoint string)- "Migrated from n8n to the Python FastAPI backend." — appears as user-facing copy under Start/Pause/Resume/Schedule Campaign.
None of these names are introduced or explained anywhere in the docs.
Consequence: Developers parsing error responses can't tell whether HASURA_SERVICE_ERROR is something they can retry, escalate, or work around. A migration-from-n8n note is internal release-notes material, not API documentation. The "sovereign" / "from India" enterprise positioning is undermined by visible vendor names in error envelopes.
The fix: Map internal errors to product-facing codes (PERSISTENCE_ERROR, TRUNK_PROVIDER_ERROR, etc.) and remove implementation-team notes ("migrated from n8n") from public docs. If VoBiz is a real product the user has to know about, add a one-paragraph definition in the Telephony overview.
14. ICE servers response example contains live TURN credentials (significant)
Location: /docs#discovery → Fetch ICE Servers
Problem: The Fetch ICE Servers expected response shows what look like real Metered.ca TURN credentials in the example:
"username": "23f5e7d82cafa25ae6f7949f",
"credential": "nr73OyVgz4Nd4ISF"
If those are placeholders, they are not marked as such. If they are real, they are a leaked shared TURN credential.
Consequence: If real, anyone reading the docs can hammer the TURN relay for free until rotated, and the bandwidth costs hit Vanira. If placeholders, AI agents and humans both treat them as real defaults and bake them into client code. Either reading is bad.
The fix: Replace with obvious placeholder strings ("username": "<turn-username>", "credential": "<turn-credential>") and add a note that TURN credentials are short-lived per request. If those particular credentials were ever live, rotate them.
15. Five widget modes documented; only one mode (voice) is documented anywhere else (significant)
Location: /docs#widget → Update Agent Widget
Problem: The mode body parameter accepts voice_only | chat_only | chat_voice | avatar_only | chat_avatar. The entire rest of the docs — SDK, Discovery, Orchestration, Tools — describes a voice/WebRTC-only product. There is no chat protocol, no avatar configuration, no inbound-message handler documented. The llms-full.txt header even says "High-fidelity, full-duplex voice SDK for browsers."
Consequence: A developer who flips mode to chat_only or avatar_only has no idea what they get, how to wire the UI, what events fire, or which providers back the avatar. Either the modes don't actually work, or they do and a major surface is undocumented.
The fix: If chat/avatar modes are shipped, add documentation for each (events, SDK callbacks, provider). If they're not shipped, remove them from the enum or mark them (coming soon).
16. WhatsApp messaging endpoint relies on undocumented inbox auth (significant)
Location: /docs#messaging → Send WhatsApp Message
Problem: POST /inbox/send requires an inbox_id of "a connected WhatsApp inbox." The error 401 INBOX_NOT_AUTHENTICATED — The inbox has not been authenticated (QR code not scanned) references a QR-scan flow that is never described anywhere. There is no list-inboxes endpoint, no create-inbox endpoint, no QR-pairing flow. WhatsApp is entirely absent from llms-full.txt.
Consequence: A developer can call this endpoint successfully only if they already provisioned the inbox via the dashboard via a flow that isn't in the docs. Anyone doing pure-API integration is stuck. Agents will try to "create" an inbox and find no endpoint.
The fix: Either document the full inbox lifecycle (create → pair via QR → list → use), or label this endpoint as dashboard-provisioned-only and direct readers to the dashboard.
17. Terminology drift: non_blocking vs fire_and_forget (significant)
Location: /docs#getting-started (Client Tool Calls), /docs#tools (Overview, End Call)
Problem: The Client Tool Calls section in Getting Started defines two execution modes: blocking and non_blocking. The Tools Overview redefines them as blocking and fire_and_forget. The End Call built-in then uses "execution_mode": "fire_and_forget".
Consequence: A developer reading the SDK section writes non_blocking into their tool definition and the API rejects it. An AI agent indexing both pages cannot determine which is the API-level string. This is exactly the failure mode the system prompt calls out: humans use judgment, agents fail silently.
The fix: Pick one (fire_and_forget matches the actual payload). Search-replace across the docs and the SDK reference. Add the enum to a single canonical "execution modes" reference.
18. Call status enum vs. campaign status enum are unrelated but unlabelled (significant)
Location: /docs#outbound-calling, /docs#campaigns
Problem: Outbound Calling documents status: triggered | in-progress | completed | failed for calls. Campaigns document status: draft | active | paused | scheduled | completed for campaigns. Both are presented simply as status — STRING with no naming differentiation.
Consequence: Not a contradiction per se — different resources can have different lifecycles — but the docs never spell out that these are distinct state machines. Developers writing dashboards or webhook routers conflate them. Worse, "completed" in both could mean different things.
The fix: Rename in prose ("call status", "campaign status") and provide a small state-diagram or transition table per resource so the two lifecycles are visibly separate.
19. Phone number formatting inconsistent across endpoints (significant)
Location: /docs#telephony
Problem: Number representations vary across the same section:
- List Numbers response:
"number": "918071387205"(no+) - Import vobiz number response:
"phone_number": "+918071387205"(with+) - Get Campaign Details
allocated_dids[].number:"918071387205"(no+) - Delete Number path:
%252B918071387205(double-encoded+) - Send WhatsApp
to:"+919182517283"
The schema notes say "E.164 format (digits only)" in one place and "E.164 phone number (e.g. +918071387205)" in another.
Consequence: A developer building a single client that round-trips numbers between endpoints must normalize manually. Equality checks fail. UI displays show +91… on one screen and 91… on another. Agents generating code copy whichever variant they saw last.
The fix: Pick one canonical form (E.164 with leading +) and use it in every example and response. Add a Conventions section near the top of the API reference that defines phone-number format once.
20. Lead resource is read-only in docs despite obviously-writeable fields (significant)
Location: /docs#leads
Problem: Only GET /leads and GET /leads/:id are documented. The response includes status, remarks, alternate_phone_number, alternate_email_id, lead_source — fields that clearly need to be created and updated somewhere. status is documented as "Current status of the lead" with no enum, no transition rules, and no mutation endpoint.
Consequence: Anyone using Vanira as a CRM-like surface for leads has no API path to create, update, or progress a lead. They must either use the dashboard or scrape the response and infer fields. AI agents have no write path to suggest.
The fix: Either add POST /leads, PATCH /leads/:id, DELETE /leads/:id (with the status enum spelled out), or explicitly note that lead writes happen via campaign uploads / dashboard only.
21. Sitemap claims everything was modified 2024-05-12 while docs content is dated 2026 (minor)
Location: /sitemap.xml
Problem: All 8 sitemap entries share <lastmod>2024-05-12</lastmod>, but multiple response examples in the docs are dated 2026-03-13, 2026-04-20, 2026-05-08, and 2026-05-01. Today is 2026-05-14.
Consequence: Search engines and AI crawlers see no freshness signal and will recrawl on a slower cadence — meaning corrections to the docs take longer to propagate. Also a credibility tell for anyone who looks.
The fix: Wire <lastmod> to actual file/page modification timestamps or set it dynamically at build time.
22. llms.txt exists but is collapsed to the marketing tagline (minor)
Location: /llms.txt
Problem: The conventional short-index llms.txt returns only the brand title and tagline — no link map, no per-section pointers, no summaries. The full file lives at /llms-full.txt. The llms.txt spec defines the short file as an index of links to the full content; this one is functionally empty.
Consequence: Crawlers and agents that follow the spec hit /llms.txt first, get nothing usable, and either give up or fall back to scraping HTML. The investment in /llms-full.txt is partially wasted.
The fix: Generate a real llms.txt index that links to /llms-full.txt, /docs (with anchor-level subsections), /api-docs (once fixed), the OpenAPI spec, and any blog/research entries you want indexed.
23. Marketing site is JS-only with no SSR text (minor)
Location: https://vanira.io/
Problem: A direct fetch of the homepage returns only the <title> and the word "VANIRA" — no nav, no feature copy, no CTAs, no links. The page is fully client-rendered.
Consequence: Crawlers without JS execution (and some embedded link previewers) see nothing. Share-card previews for the root URL are bare. Agents researching the product from the homepage have to fall back to /llms-full.txt to learn what Vanira does.
The fix: Render at least the hero, primary nav, and feature summary server-side (or via a static prerender step) so non-JS crawlers and link previewers get real content.
What they do well
- A single
/llms-full.txtexists with substantial content — most voice-AI startups don't bother, and it's how this audit was even possible. - API key model is clearly explained (sk_live / pk_live, scope split, where to attach each) with a real consequence rule ("pk attempting SIP returns 403 INSUFFICIENT_SCOPE").
- Error tables are present on most endpoints with status code + symbolic name + short cause — better than most peers, even when individual entries are wrong.
Top 3 recommendations
- Kill the dot-key duplication in every cURL example. This is the single highest-impact fix — it's the difference between an API that AI agents can use and one they can't. Audit every Create/Update body in the rendered docs.
- Publish an OpenAPI spec and either fix or remove
/api-docs. The empty SPA page that masquerades as the API reference is a worse signal than not having one at all. Pair the spec with a realllms.txtindex linking to it. - Reconcile the vendor and terminology vocabulary. Pick one of (vobiz / vonage / twilio) per response, one of (
non_blocking/fire_and_forget), one E.164 format with+, and remove internal names (Hasura, Asterisk, n8n) from public-facing error messages and prose.