CORVADevelopersOpenAPI spec →

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.

Through the API

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.

Through a phone number

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

  1. The business makes a key in Corva → Settings → Website & API keys and gives it to you. It starts with ck_ and is shown once.
  2. Put it in your server’s environment — CORVA_API_KEY — next to CORVA_API_URL=https://corva.tiruvi.site. Never ship it to the browser.
  3. Check it: GET /api/v1/health returns the business, its assistant and which features you can use.
  4. Pick your shape:
    • Corva’s assistant (how Tumble Days’ Tumbly works) — send each customer message to POST /api/v1/chat and show the reply, streamed if you like. When the assistant has a booking or callback ready it returns a proposal: show it as a card, and send the customer’s Confirm or Edit to POST /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 to POST /api/v1/chats.
  5. 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.

200Done. The body is the result.
400Something in the request is wrong. `error` says what, in words you can show a user.
401The key is missing, wrong, or revoked.
409The chat has ended (start a new sessionId), or a card was answered or replaced already.
422A card can no longer be confirmed as it is — e.g. its date has passed. Show `error` and let them edit.
413The body is over 64 KB.
429Over 120 requests a minute for this key. Back off and retry.
5xxOur 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.

GET/api/v1/health— Check your key and what you can use

The 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"
}
POST/api/v1/chat— Talk to the business's AI agent

Send 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`).

sessionIdrequiredstringYour id for this chat, 6–80 of [A-Za-z0-9_-]. Reuse it for every message in the chat.
messagerequiredstringWhat the customer typed. Up to 2,000 characters.
visitorIdstringYour visitor cookie value, to join the chat to their visit.
customerobject{ name?, phone?, email? } — what you already know about them.
streambooleanAnswer 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"
    }
  }
}
POST/api/v1/chat/confirm— The customer's Confirm or Edit on a card

Confirm 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`.

sessionIdrequiredstringThe chat's sessionId.
proposalIdrequiredstring`proposal.id` from the chat reply.
approvedrequiredbooleantrue for Confirm, false for Edit.
visitorIdstringYour 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
}
GET/api/v1/chat— Replies from a person who took the chat over

When 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.

sessionIdrequiredstringThe chat's sessionId.
afternumberOnly 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."
    }
  ]
}
POST/api/v1/leads— Send a booking, callback request or enquiry

For 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.
namerequiredstringTheir name.
phonerequiredstringTheir mobile, any common Indian format.
emailstringWhere the confirmation goes. Strongly recommended.
referencestringYour reference for this request — also the idempotency key.
servicesstring[]What needs doing.
addressstringFor a pickup or visit.
pickupDatestringYYYY-MM-DD.
timeSlotstringe.g. "8–10 AM".
preferredTimestringFor a callback, e.g. "today after 6 PM".
topicstringWhat they want to talk about.
notesstringAnything else.
promoCodestringAn offer code they used.
visitorIdstringYour visitor cookie, to join the request to their visit.
sessionIdstringThe 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
}
POST/api/v1/chats— Mirror your own chat's transcript

If 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.

sessionIdrequiredstringYour chat id.
visitorIdstringYour 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
}
POST/api/v1/visits— Record a visitor, with their cookie consent

Tell 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.

visitorIdrequiredstringYour first-party visitor id (8–80 chars).
consentrequired"necessary" | "all"What they chose on your cookie banner.
typestring"page_view" (default) or "consent".
pathstringOnly kept with consent "all".
referrerstringOnly kept with consent "all".
utmobjectutm_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
}
POST/api/v1/voice-sessions— Start a voice call from the visitor's browser

Your 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.

visitorIdstringYour visitor cookie.
namestringTheir name, if known — the assistant greets them by it.
phonestringTheir 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 up

Browsers, 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.