Put a business’s AI assistant on its website.
Corva runs an AI assistant for each business: it answers from the business’s own knowledge, stays within the limits the business sets, and turns every conversation into records the team works from — a customer, a lead with an owner, a follow-up with a time. This API is how a website plugs into it.
Your site talks to Corva from its server: chat with the assistant, send bookings and callback requests, record visitors, start voice calls in the browser. This page is for you.
No code at all. The business gets a number from Corva; whoever rings it talks to the same assistant, with the same knowledge, and lands in the same console.
Quick start
- The business makes a key in Corva → Settings → Website & API keys and gives it to you. It starts with
ck_and is shown once. - Put it in your server’s environment —
CORVA_API_KEY— next toCORVA_API_URL=https://corva.tiruvi.site. Never ship it to the browser. - Check it:
GET /api/v1/healthreturns the business, its assistant and which features you can use. - Pick your shape:
- Corva’s assistant (how Tumble Days’ Tumbly works) — send each customer message to
POST /api/v1/chatand show the reply, streamed if you like. When the assistant has a booking or callback ready it returns aproposal: show it as a card, and send the customer’s Confirm or Edit toPOST /api/v1/chat/confirm. Knowledge, bookings, the team’s follow-ups and emails all happen in Corva. - Your own assistant — keep it, and send Corva what matters: bookings and callbacks to
POST /api/v1/leads, the transcript toPOST /api/v1/chats.
- Corva’s assistant (how Tumble Days’ Tumbly works) — send each customer message to
- Optional: record visitors with their cookie consent (
/visits) and add a “talk to us” voice button (/voice-sessions).
// A tiny server-side client — Node / Next.js route handler.
export async function corva(path: string, body?: unknown) {
const res = await fetch(`${process.env.CORVA_API_URL}/api/v1/${path}`, {
method: body === undefined ? "GET" : "POST",
headers: {
Authorization: `Bearer ${process.env.CORVA_API_KEY}`,
"Content-Type": "application/json",
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error((await res.json()).error);
return res.json();
}
const { reply } = await corva("chat", { sessionId: "chat_8c1f2a", message: "Do you pick up from Sector 56?" });Keys, errors and limits
Every request carries Authorization: Bearer ck_…. A key belongs to one business; revoking it in Settings stops it at once. Requests and responses are JSON.
| 200 | Done. The body is the result. |
| 400 | Something in the request is wrong. `error` says what, in words you can show a user. |
| 401 | The key is missing, wrong, or revoked. |
| 409 | The chat has ended (start a new sessionId), or a card was answered or replaced already. |
| 422 | A card can no longer be confirmed as it is — e.g. its date has passed. Show `error` and let them edit. |
| 413 | The body is over 64 KB. |
| 429 | Over 120 requests a minute for this key. Back off and retry. |
| 5xx | Our side. Retry; /leads is safe to retry with the same reference. |
Reference
Base URL https://corva.tiruvi.site. The same endpoints as a spec: /api/v1/openapi.json.
/api/v1/health— Check your key and what you can useThe first call to make. Confirms the key, and says which features this business has — call it when your site starts, and hide the voice button when `features.voice` is false.
curl https://corva.tiruvi.site/api/v1/health \ -H "Authorization: Bearer $CORVA_API_KEY"
{
"ok": true,
"business": "Tumble Days",
"assistant": "Tumbly",
"phoneNumber": "+91 40 7573 2715",
"knowledgeDocuments": 2,
"features": {
"chat": true,
"leads": true,
"voice": true,
"voiceSecure": true
},
"apiVersion": "v1"
}/api/v1/chat— Talk to the business's AI agentSend one customer message, get the agent's reply. The agent answers only from the business's knowledge (managed in Corva), keeps within its limits, and hands over to the team when it should. Keep one `sessionId` per chat. When it has everything for a booking or a callback it returns a `proposal` instead of making it: show the details on a card with Confirm and Edit, and send the answer to `POST /api/v1/chat/confirm`. Until then nothing is booked. A new message replaces an unanswered card. With `stream: true` the answer is `text/event-stream`: `delta` events ({ text }) as the reply is written, a `proposal` event when a card should show, then `done` with the same body as the JSON answer (or `error`).
sessionIdrequired | string | Your id for this chat, 6–80 of [A-Za-z0-9_-]. Reuse it for every message in the chat. |
messagerequired | string | What the customer typed. Up to 2,000 characters. |
visitorId | string | Your visitor cookie value, to join the chat to their visit. |
customer | object | { name?, phone?, email? } — what you already know about them. |
stream | boolean | Answer as server-sent events, to show the reply as it is written. |
curl -X POST https://corva.tiruvi.site/api/v1/chat \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "chat_8c1f2a",
"message": "Pick up 3 shirts tomorrow 8–10 AM from B-402 Palm Grove, Sector 70. I'm Riya, 98765 43210",
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d"
}'{
"conversationId": "0b7c…",
"reply": "Please check the details and tap Confirm.",
"heldBy": null,
"actions": [],
"escalation": null,
"closed": false,
"proposal": {
"id": "5f0e…",
"kind": "booking",
"noun": "pickup",
"details": {
"name": "Riya",
"phone": "98765 43210",
"address": "B-402 Palm Grove, Sector 70, Gurugram",
"services": [
"Laundry"
],
"date": "2026-10-01",
"timeSlot": "8–10 AM",
"notes": "3 shirts"
}
}
}/api/v1/chat/confirm— The customer's Confirm or Edit on a cardConfirm makes what the card shows: the customer's record, a lead with an owner on the team, a follow-up on that person's list, a confirmation email to the customer and a note to the owner. The `reply` is the line to show in the chat. Edit tells the agent they want to change something — show the `reply` and let them type. Confirming the same card again returns the first result with `duplicate: true`.
sessionIdrequired | string | The chat's sessionId. |
proposalIdrequired | string | `proposal.id` from the chat reply. |
approvedrequired | boolean | true for Confirm, false for Edit. |
visitorId | string | Your visitor cookie, to join the booking to their visit. |
curl -X POST https://corva.tiruvi.site/api/v1/chat/confirm \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "chat_8c1f2a",
"proposalId": "5f0e…",
"approved": true,
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d"
}'{
"conversationId": "0b7c…",
"reply": "Done — your pickup is booked. Your reference is **TD-7K3QX9**. Kavya from our team will call you shortly to confirm. A confirmation is on its way to your inbox.",
"proposal": {
"id": "5f0e…",
"status": "confirmed"
},
"receipt": {
"reference": "TD-7K3QX9",
"owner": "Kavya Rao",
"emailed": true
},
"duplicate": false
}/api/v1/chat— Replies from a person who took the chat overWhen a person on the team takes a chat over, `POST /api/v1/chat` returns `heldBy` and no `reply`. Poll this every few seconds for what they write.
sessionIdrequired | string | The chat's sessionId. |
after | number | Only messages after this ordinal. Start at -1. |
curl https://corva.tiruvi.site/api/v1/chat?sessionId=…&after=… \ -H "Authorization: Bearer $CORVA_API_KEY"
{
"heldBy": "Kavya Rao",
"ended": false,
"messages": [
{
"ordinal": 4,
"from": "Kavya Rao",
"text": "Hi Riya, I can help with that."
}
]
}/api/v1/leads— Send a booking, callback request or enquiryFor your own forms, or your own AI's actions. Corva finds or creates the customer by phone, opens (or updates) their lead with an owner on the team, puts a follow-up on that person's list, emails the customer a confirmation and tells the owner. Safe to retry: sending the same `reference` again returns the first result with `duplicate: true`.
kindrequired | "pickup" | "callback" | "enquiry" | What they asked for. |
namerequired | string | Their name. |
phonerequired | string | Their mobile, any common Indian format. |
email | string | Where the confirmation goes. Strongly recommended. |
reference | string | Your reference for this request — also the idempotency key. |
services | string[] | What needs doing. |
address | string | For a pickup or visit. |
pickupDate | string | YYYY-MM-DD. |
timeSlot | string | e.g. "8–10 AM". |
preferredTime | string | For a callback, e.g. "today after 6 PM". |
topic | string | What they want to talk about. |
notes | string | Anything else. |
promoCode | string | An offer code they used. |
visitorId | string | Your visitor cookie, to join the request to their visit. |
sessionId | string | The chat it came from, if any. |
curl -X POST https://corva.tiruvi.site/api/v1/leads \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "pickup",
"name": "Riya Sharma",
"phone": "98765 43210",
"email": "riya@example.com",
"services": [
"Laundry: Wash, Fold & Ironing"
],
"address": "B-402, Palm Grove, Sector 70, Gurugram",
"pickupDate": "2026-10-04",
"timeSlot": "8–10 AM",
"reference": "TD-7K3QX9",
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
"sessionId": "chat_8c1f2a"
}'{
"reference": "TD-7K3QX9",
"customerId": "a734…",
"leadId": "c505…",
"followUp": {
"id": "0472…",
"assignee": "Kavya Rao",
"dueAt": "2026-10-03T05:30:00.000Z"
},
"emailed": {
"customer": true,
"team": true
},
"duplicate": false
}/api/v1/chats— Mirror your own chat's transcriptIf your site runs its own chat assistant, send the whole transcript after each reply. Corva keeps one conversation per `sessionId` (replacing the transcript each time), so the team can read every chat live and see where a request came from.
sessionIdrequired | string | Your chat id. |
visitorId | string | Your visitor cookie. |
messagesrequired | { role: "user" | "assistant", text: string }[] | The whole conversation so far. |
curl -X POST https://corva.tiruvi.site/api/v1/chats \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "chat_8c1f2a",
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
"messages": [
{
"role": "user",
"text": "Do you clean silk sarees?"
},
{
"role": "assistant",
"text": "Yes — they go to our premium dry-cleaning."
}
]
}'{
"conversationId": "e57e…",
"turns": 2
}/api/v1/visits— Record a visitor, with their cookie consentTell Corva a visitor is on the site. With `consent: "necessary"` only the visitor id is kept. With `"all"` (they accepted analytics cookies), the page, referrer and utm_ campaign are kept too, and show on the customer's record once they identify themselves. Always send the consent your banner actually recorded.
visitorIdrequired | string | Your first-party visitor id (8–80 chars). |
consentrequired | "necessary" | "all" | What they chose on your cookie banner. |
type | string | "page_view" (default) or "consent". |
path | string | Only kept with consent "all". |
referrer | string | Only kept with consent "all". |
utm | object | utm_source, utm_medium, utm_campaign… Only kept with consent "all". |
curl -X POST https://corva.tiruvi.site/api/v1/visits \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
"consent": "all",
"type": "page_view",
"path": "/",
"referrer": "https://instagram.com",
"utm": {
"utm_source": "instagram"
}
}'{
"recorded": true
}/api/v1/voice-sessions— Start a voice call from the visitor's browserYour server asks for a token (the API key must never reach the browser), and hands `token` and `bridgeUrl` to the page, which opens a WebSocket to the bridge and speaks to the assistant. Tokens last five minutes and work for one call. See “Voice calls in the browser” below for the protocol.
visitorId | string | Your visitor cookie. |
name | string | Their name, if known — the assistant greets them by it. |
phone | string | Their number, if known — joins the call to their record. |
curl -X POST https://corva.tiruvi.site/api/v1/voice-sessions \
-H "Authorization: Bearer $CORVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
"name": "Riya Sharma",
"phone": "9876543210"
}'{
"token": "eyJicmFuZElkIjoi…",
"expiresInSeconds": 300,
"bridgeUrl": "wss://voice.example.com",
"agentName": "Tumbly"
}Voice calls in the browser
Your server gets a token from POST /api/v1/voice-sessions and gives the page token and bridgeUrl. The page opens a WebSocket to the bridge and speaks. The conversation, and anything the assistant records during it, lands in the business’s console like a phone call.
→ connect new WebSocket(bridgeUrl)
→ send {"type":"start","token":"…"}
← receive {"type":"ready", "agent":"Tumbly", ...}
while the user holds "talk":
→ send binary frames — PCM16, mono, 16 kHz
when they let go:
→ send {"type":"end_turn"}
← receive binary frames — PCM16, mono, 24 kHz: the assistant's voice
← receive {"type":"heard","text":"…"} what the user said, so far
← receive {"type":"said","text":"…"} what the assistant said, so far
← receive {"type":"turn_complete"}
← receive {"type":"held","by":"Kavya Rao"} a person took over; stop playback
← receive {"type":"human","name":"…","text":"…"} what they typed
← receive {"type":"closed","reason":"…"} / {"type":"error","message":"…"}
→ send {"type":"stop"} hang upBrowsers, and iPhones in particular. The microphone only works on an https page (or localhost) — on plain http, Safari never asks for permission and navigator.mediaDevices is missing. Ask for the microphone and create your AudioContext in the tap handler itself, before any await, or iOS will neither prompt nor play sound. Use the device’s own sample rate and resample to 16 kHz yourself; iOS does not honour a requested rate. And the bridge must be wss:// for an https page —/health tells you (features.voiceSecure).
Cookies & consent
Corva does not set cookies on your site. We suggest two first-party cookies, both strictly necessary: a random visitor id (so a chat, a request and a call from one browser join up — send it as visitorId), and the visitor’s choice on your cookie banner. Send that choice as consent on every /visits call: with "necessary" Corva keeps only the id; with "all" it also keeps pages, referrer and campaign, and shows them on the customer’s record once they tell you who they are. Changing to "necessary" later forgets what was kept.
Corva API v1 · questions: ask whoever at the business gave you the key, or Corva support.