# Corva developer guide > Connect a business's website and systems to its Corva AI assistant: chat, bookings and leads, customers, order records, payments, visitors, voice, and webhooks. API v1, base URL https://corva.tiruvi.site. Source: https://corva.tiruvi.site/developers Contents: - [Overview](#overview), What Corva is, and the three ways a website or system connects to it. - [Quick start](#start), A key, two settings, and a chat route on your server. - [How it fits together](#concepts), Sessions and visitors, when a chat ends, one customer across channels, the leads board, details, data out, bookings, takeover. - [Keys, errors and limits](#auth), Bearer keys, status codes, rate limits, timeouts and formats. - [API reference](#reference), Every endpoint, with its fields, a request and a response. - [Voice calls in the browser](#voice), Tokens, the WebSocket protocol, iPhone gotchas and limits. - [Webhooks](#webhooks), Events Corva sends your systems, and how to verify each delivery. - [Taking payments](#payments), Your payment endpoint, offers checked in code, and reporting a payment back. - [Cookies & consent](#cookies), The two first-party cookies we suggest, and what consent changes. - [Going live](#golive), The checklist before real customers use it. ## Overview Corva runs an AI assistant for each business. It answers from the business's own knowledge, stays within the limits the business sets, collects the details the business asks for, and turns every conversation into records the team works from: a customer, a lead with an owner, a follow-up with a time. A person on the team can take any chat or call over, live. This API is how a website and the business's other systems plug into that. Three ways to connect, most sites start with the first: - **Corva's assistant on your site.** Your chat window, Corva's brain. Send each message to `/chat`, show the reply, show a card when a booking is ready. - **Your own assistant, Corva behind it.** Keep the bot or the forms you have. Send bookings and callbacks to `/leads` and transcripts to `/chats`; Corva gives them owners, follow-ups and emails. - **Voice on your site.** A call button: your server fetches a short-lived token, the page talks to the assistant over a WebSocket, and the business's team can take the call over and speak. Base URL: `https://corva.tiruvi.site`. Machine-readable: the OpenAPI spec at [https://corva.tiruvi.site/api/v1/openapi.json](https://corva.tiruvi.site/api/v1/openapi.json), this whole guide as Markdown at [https://corva.tiruvi.site/developers.md](https://corva.tiruvi.site/developers.md), and an index for AI tools at [https://corva.tiruvi.site/llms.txt](https://corva.tiruvi.site/llms.txt). ## 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`. These are the only two settings an integration needs. Never ship the key to the browser. 3. Check it: `GET /api/v1/health`. Then read `GET /api/v1/config` for the assistant's name, what can be booked and the details the business collects. 4. Add a route on your server that forwards the visitor's message to `POST /api/v1/chat` and returns the reply. When the reply carries a `proposal`, show it as a card; send Confirm or Edit to `POST /api/v1/chat/confirm`. When the customer closes the chat or reloads the page, send `POST /api/v1/chat/end`. 5. Optional: voice calls (`/voice-sessions`), visitor tracking with consent (`/visits`), order status for the assistant to answer from (`/records`), payments (`/payments`), and a webhook so your own systems hear about new leads. ```ts // 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(20_000), }); if (!res.ok) throw new Error((await res.json()).error); return res.json(); } const { assistant, fields, booking } = await corva("config"); const turn = await corva("chat", { sessionId, message, visitorId }); // turn.reply → show it // turn.proposal → show a card; on Confirm: await corva("chat/confirm", { sessionId, proposalId: turn.proposal.id, approved: true }); // The window closed, or the page is unloading (sendBeacon to your own route): await corva("chat/end", { sessionId }); ``` ## How it fits together ### Sessions and visitors A `sessionId` is one chat, you make it up, reuse it for every message, and start a new one for a new chat. A `visitorId` is one browser, kept in a first-party cookie you set. Send it on everything and Corva joins a visitor's page views, chats, bookings and voice calls into one customer record once they say who they are. ### When a chat ends A call hangs up; a chat window just closes. Send `POST /api/v1/chat/end` when it does, from a `pagehide` handler, with `navigator.sendBeacon` to your own server route, and the conversation is closed, summarised and sent to your `conversation.ended` webhook at once. If you do not, Corva ends it when the same `visitorId` starts a new `sessionId`, or after 30 quiet minutes. Either way that `sessionId` is finished: the next message answers `409`, so start a new one. ### One customer, every channel: and who is proven Corva keeps one customer record across website chat, voice, phone, WhatsApp, email and what your system sends. Every number and email they have used is on it, each marked **verified** (they wrote from it on WhatsApp or email, they called from it, your system sent it, or they entered a code sent to it) or **stated** (typed into a chat). A browser that was identified before starts its next chat as that customer. What you pass as `customer` on `/chat` is **stated**: the chat joins that record, but the assistant reads out what is on file (an address, past payments) and asks for a payment only once the customer proves who they are, it sends a six-digit code by SMS or email to the number or email *already on file*, never to one typed in the chat, and the model never sees the code. Records you send to `/records` and `/payments` count as verified. Two customers are never merged on a claim; a likely match goes to the team. ### The leads board Five stages, the same for every business: `new` (New), `contacted` (Contacted), `proposal` (Processing), `won` (Converted), `lost` (Lost). A customer saying who they are makes a New lead, and so does a booking (it waits there for the customer to confirm it from its email: `customerConfirmedAt`); a payment reported `paid` makes it Converted. Move it to Processing and Converted from your own system with `POST /api/v1/leads/{id}`; Lost is only ever a person's call. Older leads may say `qualified`, read it as Processing. ### Details to collect Each business keeps a list of what it needs from a customer, an address, a request type, a budget. Its team edits the list in Corva; you read it from `/config` as `fields`. The assistant asks for them in chat and on calls; your own forms can show the same fields and send answers as `details` (`{ key: value }`). Every chat reply reports what has been collected so far. Do not hard-code the list: it changes without a deploy. ### Getting the data out Two ways, and most integrations use both. **Webhooks** push each lead, follow-up and handoff to your system the moment it happens. The **Records** endpoints let you read it back whenever you like, list leads changed since your last sync, look a customer up by phone, pull a transcript. Both carry the same two things: the business's own `details` by field key, and `request`, the booking or callback as fields (date, time slot, services, address, reference) rather than a sentence. Filter on anything collected: `GET /api/v1/leads?details.service_type=AC%20service`. ### Bookings are confirmed by the customer The assistant never books on its own say-so. It returns a `proposal`, a booking (the business's own word for it is `proposal.noun`: a pickup, an appointment, a visit) or a callback, with the details to show. Nothing exists until the customer confirms; then Corva creates the customer, the lead, its owner and the follow-up, and sends the emails. If the customer types instead of tapping, the card is replaced. ### A person can take over Anyone on the business's team can take a chat or a call from the assistant in Corva. In a chat, replies then come from `GET /api/v1/chat` under that person's name. On a voice call the assistant says it is transferring the caller, and the person's voice arrives over the same connection, your page keeps playing the audio it is sent. ### Your customers' privacy (India's DPDP Act) For your customers' data the business is the data fiduciary; Corva processes it for the business. Before the first message or call, show a short notice in the chat window: that the customer is talking to an AI assistant, what it keeps (what they type or say, their contact details), why, and a link to the business's privacy policy and to how they can ask to see, correct or delete it. When a customer asks, the business answers from Corva: **Download their data** and **Erase this customer** on the customer's record, and `customer.erased` tells your system to erase them too. A business whose customers may be under 18 (a school, a children's clinic) needs a parent's verifiable consent unless the law exempts what it does; the assistant cannot check age, so such a business asks the parent to make contact. ## 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 (a streamed chat reply is `text/event-stream`). | Status | Meaning | | --- | --- | | `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. | | `403` | The key works but lacks the scope this route needs (`website`, `read`, `write` or `webhooks`; chosen when the key is made). | | `402` | The business's plan has no room for a new conversation. `error` is safe to show a customer; treat the assistant as offline. | | `404` | No conversation or card with that id. | | `409` | The chat has ended (start a new sessionId), or a card was answered or replaced already. | | `413` | The body is over 64 KB. | | `422` | A card can no longer be confirmed as it is, e.g. its date has passed. Show `error` and let them edit. | | `429` | Over 300 requests a minute for this business, one chat sending more than 12 messages a minute, or more than 120 new chats an hour for this business. Back off and retry. | | `5xx` | Our side. Retry; /leads is safe to retry with the same reference, /chat/confirm with the same proposalId. | Timeouts: a chat reply usually starts within two to four seconds and can take longer when the model is busy, allow 30 seconds, or stream. Phone numbers are accepted in any common Indian format and returned as `+91 98765 43210`. Dates are `YYYY-MM-DD` in India time; timestamps are ISO 8601 UTC. ## API reference Base URL `https://corva.tiruvi.site`. The same endpoints as a spec: [/api/v1/openapi.json](https://corva.tiruvi.site/api/v1/openapi.json). Example values are from a made-up home-services business; a real business's fields and words come from `/config`. ### Setup, Check the key, and read how the business is set up. #### `GET /api/v1/health`, Check your key The first call to make. Confirms the key belongs to a business and says which features are on. Cheap enough to call when your server starts. Request: ```bash curl https://corva.tiruvi.site/api/v1/health \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "ok": true, "business": "Acme Home Services", "businessId": "3f2c…", "assistant": "Ava", "phoneNumber": "+91 40 7573 2715", "knowledgeDocuments": 3, "features": { "chat": true, "leads": true, "voice": true, "voiceSecure": true }, "apiVersion": "v1" } ``` #### `GET /api/v1/config`, How the business is set up Everything your site needs so it does not hard-code the business: the assistant's name, what can be booked, and `fields`, the business's Details to collect, which its team edits in Corva. Build your own forms from `fields` and send answers back as `details` (keyed by `key`); the assistant asks for the same ones in chat and on calls. Read it at start-up or cache it for a few minutes: when the business adds a field, your site picks it up without a deploy. Request: ```bash curl https://corva.tiruvi.site/api/v1/config \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "business": "Acme Home Services", "businessId": "3f2c…", "assistant": "Ava", "industry": { "key": "home_services", "label": "Home services" }, "phoneNumber": "+91 40 7573 2715", "features": { "chat": true, "leads": true, "voice": true, "voiceSecure": true }, "booking": { "noun": "visit", "needsAddress": true, "maxDaysAhead": 30 }, "fields": [ { "key": "name", "label": "Name", "hint": null, "kind": "text", "options": [], "required": true }, { "key": "phone", "label": "Phone number", "hint": "A mobile number to reach them on", "kind": "phone", "options": [], "required": true }, { "key": "email", "label": "Email", "hint": "Where confirmations go", "kind": "email", "options": [], "required": false }, { "key": "address", "label": "Address", "hint": "Flat, building and area", "kind": "address", "options": [], "required": true }, { "key": "service_type", "label": "Service", "hint": null, "kind": "choice", "options": [ "AC service", "Plumbing", "Electrical" ], "required": true } ], "apiVersion": "v1" } ``` ### Chat, The assistant in your chat window, or your own assistant's transcripts. #### `POST /api/v1/chat`, Send a customer's message to the assistant One message in, the assistant's reply out. It answers only from the business's knowledge (kept in Corva), stays within the limits the business set, collects the business's details as the conversation goes, and hands over to a person when it should. Keep one `sessionId` per chat. When it has what a booking or a callback needs, it does not make it, it returns a `proposal`. Show that as a card with Confirm and Edit and send the customer's answer to `POST /api/v1/chat/confirm`. A new message replaces an unanswered card. `details` is everything collected so far, keyed by the business's field keys. `heldBy` is set once a person on the team has taken the chat over: `reply` is then null, and their replies come from `GET /api/v1/chat`. With `stream: true` the answer is `text/event-stream`: `delta` events (`{ text }`) while the reply is written, a `proposal` event when a card should show, then `done` with the same body as the JSON answer, or `error`. **Bookings.** For a business that has set up things to book (Bookings in Corva), the assistant checks real open times and may send a `slots` event, `{ bookingTypeId, type, date, day, times: [{ time, label }] }`, which you can show as buttons that send, say, `"11:00 am on Wednesday, 7 Oct"`. Its card then has `kind: "appointment"` with `details: { type, date, time, when, name, email?, phone?, notes? }`. Confirming books the slot; if it filled in the meantime the confirm returns 409 with the times still open. Ignore events you do not recognise. | Body field | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | Your id for this chat: 6–80 of [A-Za-z0-9_-]. Reuse it for every message in the chat; start a new one for a new chat. | | `message` **(required)** | string | What the customer typed. Up to 2,000 characters. | | `visitorId` | string | Your first-party visitor id, to join the chat to their visits and later calls. | | `customer` | object | { name?, phone?, email?, userId?, app? }, what you already know about them. For a user **you authenticated**, send `idToken` (your login's signed token; verified by Corva, recommended) or, from your server, `userId` (and `app`) after `POST /accounts` linked it: Corva then treats them as proven and the assistant can answer from their record without a code. A business can turn on "only trust signed tokens" and then `userId` alone identifies nobody. `phone`/`email` alone are only stated, not proof. The assistant will not ask again. Corva treats these as *stated*, not proven: the chat joins that customer's record, but the assistant reads out what is on file (an address, payments) and asks for a payment only after the customer enters a code sent to the number or email on file. | | `stream` | boolean | Answer as server-sent events, to show the reply as it is written. | Request: ```bash 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": "My AC is leaking. Can someone come tomorrow morning? I'm Riya, 98765 43210, B-402 Palm Grove, Sector 70", "visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d" }' ``` Response: ```json { "conversationId": "0b7c…", "reply": "Please check the details below and tap Confirm.", "heldBy": null, "actions": [], "escalation": null, "closed": false, "proposal": { "id": "5f0e…", "kind": "booking", "noun": "visit", "details": { "name": "Riya", "phone": "98765 43210", "address": "B-402 Palm Grove, Sector 70", "services": [ "AC service" ], "date": "2026-10-04", "timeSlot": "8–10 AM", "notes": "AC leaking" }, "extra": [ { "label": "Units", "value": "1 split AC" } ] }, "details": { "name": "Riya", "phone": "+91 98765 43210", "address": "B-402 Palm Grove, Sector 70", "service_type": "AC service" } } ``` #### `POST /api/v1/chat/location`, The visitor shared where they are Call from your server when the visitor taps your Share location button (the browser's location, or a pin on your map) after the chat has started. Corva keeps the point on the customer, or holds it on the chat until they give a name or number. Send the returned `message` as the visitor's next chat message so the assistant knows and carries on, the same as a location shared on WhatsApp. `consent` must be `true`: show a line saying why you ask. With `saveAs` (Home, Work) and an `address`, it is also saved as one of the customer's addresses. | Body field | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | The chat's sessionId. | | `lat` **(required)** | number | Latitude. | | `lng` **(required)** | number | Longitude. | | `consent` **(required)** | boolean | True: the visitor agreed to share it. | | `source` | string | gps (the browser's location) or pin (dropped on a map). Default pin. | | `accuracyM` | number | The browser's accuracy in metres, for gps. | | `address` | string | The readable address for the point, if your map gave one. | | `pincode` | string | Its six-digit PIN code, if known. | | `saveAs` | string | Save as a named address: Home, Work. Needs `address`. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/chat/location \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sessionId": "web-3f9a1c", "lat": 28.4089, "lng": 77.0507, "accuracyM": 18, "source": "gps", "consent": true, "address": "B-204, Palm Court, Sector 57, Gurugram", "pincode": "122003", "saveAs": "Home" }' ``` Response: ```json { "stored": "customer", "savedAsAddress": true, "message": "I have shared my location: B-204, Palm Court, Sector 57, Gurugram (https://maps.google.com/?q=28.4089,77.0507)" } ``` #### `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. `reply` is the line to show in the chat and `receipt` is for the card. Edit tells the assistant they want to change something, show the `reply` and let them type. Confirming the same card twice returns the first result with `duplicate: true`. | Body field | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | The chat's sessionId. | | `proposalId` **(required)** | string | `proposal.id` from the chat reply. | | `approved` **(required)** | boolean | true for Confirm, false for Edit. | | `visitorId` | string | Your visitor id, to join the booking to their visits. | Request: ```bash 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" }' ``` Response: ```json { "conversationId": "0b7c…", "reply": "Done, your visit is booked. Your reference is **AH-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": "AH-7K3QX9", "owner": "Kavya Rao", "emailed": true }, "duplicate": false } ``` #### `GET /api/v1/chat`, Replies from a person who took the chat over Anyone on the business's team can take a chat over from the assistant in Corva. Poll this, every eight seconds or so while a chat is open, every three once `heldBy` is set, and show messages whose `from` is not "assistant" under that person's name. `heldBy` goes back to null when they hand the chat back; `ended` means start a new sessionId. | Query parameter | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | The chat's sessionId. | | `after` | number | Only messages after this ordinal. Start at -1, then pass the highest ordinal you have seen. | Request: ```bash curl https://corva.tiruvi.site/api/v1/chat?sessionId=…&after=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "heldBy": "Kavya Rao", "ended": false, "messages": [ { "ordinal": 6, "from": "Kavya Rao", "text": "Hi Riya, Kavya here, I can get someone to you by 9." } ] } ``` #### `POST /api/v1/chat/end`, The customer has left the chat A website chat has no hang-up, so tell Corva when it is over: the chat window was closed, the page was reloaded, or the customer started a new chat. The conversation is closed at once, resolved if the customer said anything, abandoned if not, and summarised, and `conversation.ended` goes to your webhooks. Use `navigator.sendBeacon` to your own server route on `pagehide`, which then calls this with the key. If you never call it, nothing breaks: a chat is also ended when the same `visitorId` starts a new `sessionId`, or after 30 quiet minutes. Ending a chat that is already over, or a `sessionId` Corva has not seen, does nothing (`ended: false`). After it, that `sessionId` answers `409`: start a new one. | Body field | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | The chat's sessionId. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/chat/end \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sessionId": "chat_8c1f2a" }' ``` Response: ```json { "ended": true } ``` #### `POST /api/v1/chats`, Mirror your own assistant's transcript Only if your site runs its own chat assistant instead of Corva's. Send the whole transcript after each reply; Corva keeps one conversation per `sessionId`, replacing it each time, so the team can read every chat live and see where a request came from. | Body field | Type | Description | | --- | --- | --- | | `sessionId` **(required)** | string | Your chat id. | | `visitorId` | string | Your visitor id. | | `messages` **(required)** | { role: "user" \| "assistant", text: string }[] | The whole conversation so far. | Request: ```bash 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 service window ACs?" }, { "role": "assistant", "text": "Yes, window and split units both." } ] }' ``` Response: ```json { "conversationId": "e57e…", "turns": 2 } ``` ### Leads, Bookings, callbacks and enquiries from your own forms. #### `POST /api/v1/leads`, Send a booking, callback request or enquiry For your own forms, a booking form, a "call me back" button, a contact page. 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. `kind: "booking"` is whatever the business books (see `booking.noun` in /config). Every request makes the lead **New**; a booking with an `email` gets a confirmation email with a link where the customer confirms it, corrects it and shares their location, after which the lead's `customerConfirmedAt` is set and `lead.updated` is sent. Send the business's own fields as `details`, keyed by the `key`s from /config, unknown keys are ignored, choices are matched to the business's options. Safe to retry: the same `reference` again returns the first result with `duplicate: true`. | Body field | Type | Description | | --- | --- | --- | | `kind` **(required)** | "booking" \| "callback" \| "enquiry" | What they asked for. ("pickup" is accepted as another word for "booking".) | | `name` **(required)** | string | Their name. | | `phone` **(required)** | string | Their mobile, in any common Indian format. | | `email` | string | Where the confirmation goes. Strongly recommended. | | `reference` | string | Your reference for this request, also the idempotency key. Corva makes one if you do not. | | `details` | object | The business's Details to collect: { key: value }, keys from GET /config. | | `services` | string[] | What they want done, for a booking. | | `address` | string | Where, when the booking needs an address. | | `pincode` | string | Six digits (also read from the end of `address`). For a business that serves by PIN code, a PIN code it does not serve is refused with a 400; the PIN code and its store are kept on the customer. | | `date` | string | For a booking: YYYY-MM-DD. | | `timeSlot` | string | For a booking, 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 id, to join the request to their visits. | | `sessionId` | string | The chat it came from, if your own assistant took it (see /chats). | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/leads \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "booking", "name": "Riya Sharma", "phone": "98765 43210", "email": "riya@example.com", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM", "details": { "service_type": "AC service", "units": "2 split ACs" }, "visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d" }' ``` Response: ```json { "reference": "AH-7K3QX9", "customerId": "a734…", "leadId": "c505…", "isNew": true, "followUp": { "id": "0472…", "assignee": "Kavya Rao", "dueAt": "2026-10-03T05:30:00.000Z" }, "emailed": { "customer": true, "team": true }, "duplicate": false } ``` ### Records, Read the business's leads, customers and conversations back; keep a lead in step with your system; tell the assistant where an order has got to. #### `GET /api/v1/leads`, List and filter leads The business's leads, newest-changed first, whichever way they arrived: chat, a call, your forms, or added by hand. Each carries `details` (the business's own Details to collect, by field key) and `request` (the latest booking, callback or enquiry as fields: date, time slot, services, address, reference). Filter on any collected detail with `details.=`, keys from `/config`, matched case-insensitively; `details.=` with no value means "has an answer". Because details are stored by key, a field the business adds tomorrow is filterable tomorrow with no change here. Page with `cursor`: pass back `nextCursor` until it is null. For a sync, poll `updatedSince` with the time of your last run. | Query parameter | Type | Description | | --- | --- | --- | | `stage` | string | One or more of new, contacted, proposal, won, lost, comma-separated (New, Contacted, Processing, Converted, Lost on the board). `qualified`, an older key, is accepted and counts as `proposal`; older leads may still come back with `stage: "qualified"`, treat it as Processing. `stageLabel` is the board's word for the stage. | | `since` | string | Created at or after this time (ISO 8601). | | `updatedSince` | string | Changed at or after this time, what a sync asks for. | | `details.` | string | A collected detail equals this, e.g. details.service_type=AC service. Repeat for several. | | `phone` | string | The lead's phone number, any format. | | `reference` | string | The reference a booking or callback was given. | | `q` | string | Text in the name, the request or the notes. | | `limit` | number | 1–100, default 25. | | `cursor` | string | `nextCursor` from the previous page. | Request: ```bash curl https://corva.tiruvi.site/api/v1/leads?stage=…&since=…&updatedSince=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "leads": [ { "id": "c505…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "interest": "AC service · visit 2026-10-04, 8–10 AM · B-402, Palm Grove, Sector 70", "notes": "Ref AH-7K3QX9", "stage": "new", "stageLabel": "New", "valueRupees": null, "source": "web_chat", "createdByAi": true, "owner": "Kavya Rao", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" }, "customerConfirmedAt": "2026-10-03T11:12:00.000Z", "customerId": "a734…", "conversationId": "0b7c…", "createdAt": "2026-10-03T04:31:12.000Z", "updatedAt": "2026-10-03T04:31:12.000Z" } ], "nextCursor": null } ``` #### `GET /api/v1/leads/{id}`, One lead A lead with its customer and its follow-ups, who owes the customer what, by when, and whether it was done. Request: ```bash curl https://corva.tiruvi.site/api/v1/leads/{id} \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "lead": { "id": "c505…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "interest": "AC service · visit 2026-10-04, 8–10 AM · B-402, Palm Grove, Sector 70", "notes": "Ref AH-7K3QX9", "stage": "new", "stageLabel": "New", "valueRupees": null, "source": "web_chat", "createdByAi": true, "owner": "Kavya Rao", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" }, "customerConfirmedAt": "2026-10-03T11:12:00.000Z", "customerId": "a734…", "conversationId": "0b7c…", "createdAt": "2026-10-03T04:31:12.000Z", "updatedAt": "2026-10-03T04:31:12.000Z" }, "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70" }, "followUps": [ { "id": "0472…", "title": "Confirm visit with Riya Sharma for 2026-10-04, 8–10 AM", "detail": "Ref AH-7K3QX9", "dueAt": "2026-10-03T05:30:00.000Z", "status": "open", "assignee": "Kavya Rao", "outcome": null, "completedAt": null } ] } ``` #### `POST /api/v1/leads/{id}`, Update a lead from your system Keep Corva in step with your own system: when work starts, move the lead to `proposal` (Processing); when it is done, to `won` (Converted); add what your team learnt. Send only what changes. Returns the lead as `GET` does, and tells webhooks (`lead.updated`) like a change made in the console. The board is the same five stages for every business. Corva moves a lead itself in three places: a customer telling the assistant who they are makes it New, a confirmed booking makes it Contacted, and a payment reported `paid` makes it Converted. Lost is only ever a person's (or your system's) decision. | Body field | Type | Description | | --- | --- | --- | | `stage` | string | new, contacted, proposal (Processing), won (Converted) or lost. `qualified` is accepted as `proposal`. | | `lostReason` | string | Why, when the stage is lost. | | `notes` | string | Replaces the lead's notes. | | `valueRupees` | number | What the lead is worth, in rupees; null to clear. | | `details` | object | { key: value }, merged into the lead's details; keys from GET /config. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/leads/{id} \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "stage": "won", "valueRupees": 1800, "details": { "units": "2 split ACs" } }' ``` Response: ```json { "lead": { "id": "c505…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "interest": "AC service · visit 2026-10-04, 8–10 AM · B-402, Palm Grove, Sector 70", "notes": "Ref AH-7K3QX9", "stage": "won", "stageLabel": "Converted", "valueRupees": 1800, "source": "web_chat", "createdByAi": true, "owner": "Kavya Rao", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" }, "customerConfirmedAt": "2026-10-03T11:12:00.000Z", "customerId": "a734…", "conversationId": "0b7c…", "createdAt": "2026-10-03T04:31:12.000Z", "updatedAt": "2026-10-03T04:31:12.000Z" }, "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70" }, "followUps": [] } ``` #### `POST /api/v1/records`, Tell the assistant where an order has got to What happens after a booking happens in your system: collected, in progress, out for delivery. Send each order's current state here whenever it changes, under the reference the customer was given, and the assistant can answer "where is my order?" in chat and on calls. The same `kind` and `reference` replace what was there. `status` is read to the customer as it is, write it as a sentence. Name the customer by `customer.id`, `leadId` or `customer.phone`. | Body field | Type | Description | | --- | --- | --- | | `reference` **(required)** | string | The id the customer was given. | | `label` **(required)** | string | What it is, in a few words. | | `status` | string | Where it has got to, as a sentence the customer can be told. | | `kind` | string | order (default), booking, delivery, ticket, invoice or subscription. | | `amountRupees` | number | The bill, when there is one. | | `leadId` | string | The lead it grew out of. | | `customer` | object | { id } or { phone, name?, email? }, found by phone, or created. | | `meta` | object | Anything else worth knowing, as fields: stage, expected times, driver. The assistant may use it; never put an address or phone number here. | | `occurredAt` | string | When the record began; defaults to now. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/records \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reference": "AH-7K3QX9", "label": "AC service", "status": "The technician is on the way, expected around 5:40 pm.", "leadId": "8f3b2c1a-…", "meta": { "stage": "en_route", "expectedArrival": "2026-10-04T12:10:00.000Z" } }' ``` Response: ```json { "record": { "id": "5a1e…", "kind": "order", "reference": "AH-7K3QX9", "label": "AC service", "status": "The technician is on the way, expected around 5:40 pm.", "amountRupees": null, "customerId": "c1d2…", "occurredAt": "2026-10-03T04:30:00.000Z", "meta": { "stage": "en_route", "expectedArrival": "2026-10-04T12:10:00.000Z" } }, "created": false } ``` #### `GET /api/v1/records`, Records under a reference What Corva currently holds under a reference, what the assistant would tell a customer who asked. | Query parameter | Type | Description | | --- | --- | --- | | `reference` | string | The reference, in any spacing or case. | Request: ```bash curl https://corva.tiruvi.site/api/v1/records?reference=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "records": [ { "id": "5a1e…", "kind": "order", "reference": "AH-7K3QX9", "label": "AC service", "status": "The technician is on the way, expected around 5:40 pm.", "amountRupees": null, "customerId": "c1d2…", "occurredAt": "2026-10-03T04:30:00.000Z", "meta": {} } ] } ``` #### `POST /api/v1/notices`, Send a customer a message about their order Tell a customer their order has moved (pickup booked, work started, ready, payment due) on WhatsApp and by email, from the business's own number and sender. You write the message; Corva delivers it. WhatsApp only lets a business write freely within 24 hours of the customer's last message, so outside that window the message goes by email only (the response says which channels went). The same `key` sends once, however often you post it. Name the customer by `customer.id`, `leadId`, `customer.phone` or `customer.email`. | Body field | Type | Description | | --- | --- | --- | | `key` **(required)** | string | Your own id for this message, such as "TD-7K3QX9:ready". Posting it again sends nothing. | | `kind` **(required)** | string | pickup, started, ready, payment_due or bill. | | `text` **(required)** | string | The message, as the customer should read it (600 characters at most). | | `reference` | string | The order reference, shown in the email's subject. | | `link` | string | An https address: the tracking page, or the payment link for payment_due. Added to the message. | | `linkLabel` | string | The email button's words (30 characters), such as "Pay now"; defaults to one that fits the kind. | | `customer` | object | { id } or { phone, email?, name? }. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/notices \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "TD-7K3QX9:ready", "kind": "ready", "reference": "TD-7K3QX9", "text": "Hi Asha, your order TD-7K3QX9 is ready.", "link": "https://orbo.tiruvi.site/t/…", "customer": { "phone": "+919811100777", "email": "asha@example.com" } }' ``` Response: ```json { "duplicate": false, "channels": { "whatsapp": { "sent": false, "reason": "WhatsApp is not connected, or the customer has not written in the last 24 hours (a template is needed)." }, "email": { "sent": true } } } ``` #### `GET /api/v1/summary`, The business at a glance Totals for the last `days` days (1–90, default 7): customers who reached out and how many were new, conversations and who answered them (the assistant or the team) by channel and by day, leads opened, won and lost, follow-ups overdue and due today, handoffs waiting, and each person's calls, chats, follow-ups and wins. For an owner's dashboard on the business's own site; counts only, no transcripts or customer names. Every figure matches Corva's Overview screen, linked in `links`. | Query parameter | Type | Description | | --- | --- | --- | | `days` | number | How many days back, 1–90. Default 7. | Request: ```bash curl https://corva.tiruvi.site/api/v1/summary?days=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "period": { "from": "2026-09-30", "to": "2026-10-06", "days": 7 }, "customers": { "total": 412, "new": 23 }, "conversations": { "total": 96, "ai": 81, "people": 13, "waiting": 2 }, "leads": { "open": 18, "new": 31, "byAssistant": 27, "won": 22, "lost": 4, "pipelinePaise": 1840000 }, "followUps": { "overdue": 2, "dueToday": 5, "upcoming": 9, "doneLate": 1 }, "handoffs": { "waiting": 0 }, "team": { "atWorkNow": 3, "people": [ { "name": "Kavya", "role": "manager", "calls": 12, "chats": 30, "followUpsDone": 14, "leadsWon": 6, "daysPresent": 6 } ] }, "links": { "overview": "https://corva.tiruvi.site/app/overview" } } ``` #### `GET /api/v1/team`, The business's team Everyone with access to the business: name, email, phone, the role they hold in Corva and their status. `customRole` is the name of their custom role in Corva, if any, and `corvaAccess` is false when that role does not use Corva at all (a driver, a cleaner). When the business uses Orbo for orders and deliveries, `delivery` says what the person is there, `driver`, `staff`, `store_manager` or `regional_manager`, and `places`, the codes of their stores (region codes for a regional manager); `via` says whether it was set on the person or comes from their Corva role. It is `null` for people with no role in Orbo. Orbo reads this so a business adds a person once, in Corva. People are added and given a delivery role on Corva's Team screen. Request: ```bash curl https://corva.tiruvi.site/api/v1/team \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "team": [ { "id": "9d21…", "name": "Ravi Kumar", "email": "ravi@example.com", "phone": "919811120456", "role": "analyst", "customRole": "Driver", "corvaAccess": false, "status": "active", "delivery": { "role": "driver", "places": [ "SEC57" ], "via": "role" } }, { "id": "c505…", "name": "Kavya Rao", "email": "kavya@example.com", "phone": null, "role": "manager", "customRole": null, "corvaAccess": true, "status": "active", "delivery": { "role": "store_manager", "places": [ "SEC57", "SEC14" ], "via": "person" } }, { "id": "a734…", "name": "Neha Shah", "email": "neha@example.com", "phone": null, "role": "agent", "customRole": null, "corvaAccess": true, "status": "active", "delivery": null } ] } ``` #### `GET /api/v1/catalog`, Products and services The business's price list in the same shape as the downloadable template: `category`, `name`, `price` (rupees, null when quoted), `unit`, `description`, `available`, `sku`, `gstPercent`, `hsn`, `priceIncludesGst`. Needs the `read` scope. Orbo reads this to stay in step. Request: ```bash curl https://corva.tiruvi.site/api/v1/catalog \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "items": [ { "category": "Wash & fold", "name": "Wash and fold", "price": 99, "unit": "per kg", "description": "Ready in 48 hours", "available": true, "sku": "", "gstPercent": null, "hsn": "", "priceIncludesGst": true } ] } ``` #### `POST /api/v1/catalog`, Add and update products and services Send the list in the template's shape. Items are matched by category and name, so sending again updates prices instead of doubling the list; nothing is ever removed. At most 2,000 at a time. Needs the `write` scope. Orbo sends its list here. | Body field | Type | Description | | --- | --- | --- | | `items` **(required)** | array | Each: { category?, name, price?, unit?, description?, available?, sku?, gstPercent?, hsn?, priceIncludesGst? }, the template's columns. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/catalog \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "category": "Wash & fold", "name": "Wash and fold", "price": 99, "unit": "per kg", "available": true } ] }' ``` Response: ```json { "created": 1, "updated": 0, "skipped": 0 } ``` #### `GET /api/v1/customers`, List and find customers Everyone the business has a record for, newest first. Look one up by `phone` or `email` to recognise a returning customer in your own app, these match the number and email on file. Corva keeps one customer across every channel (chat, calls, WhatsApp, email, your records): a number or email someone only typed in a chat is kept on their record as *stated* but never becomes the one on file, and two customers are never merged on a claim alone. | Query parameter | Type | Description | | --- | --- | --- | | `phone` | string | Their phone number, any format. | | `email` | string | Their email address. | | `q` | string | Text in the name, email or phone. | | `since` | string | Created at or after this time. | | `limit` | number | 1–100, default 25. | | `cursor` | string | `nextCursor` from the previous page. | Request: ```bash curl https://corva.tiruvi.site/api/v1/customers?phone=…&email=…&q=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "customers": [ { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70", "owner": "Kavya Rao", "leads": 2, "openLeads": 1, "lastConversationAt": "2026-10-03T04:28:40.000Z", "createdAt": "2026-09-12T10:02:11.000Z" } ], "nextCursor": null } ``` #### `POST /api/v1/customers/email`, Confirm an email for a phone-only customer Verification codes go only to an email the business has *confirmed* for the customer, so a customer with only a phone number cannot be sent one. This emails them a single-use link; pressing its button confirms that address for them. **Call it only for a person your own system has already identified** (someone at your counter, a user signed in to your app). The link proves the mailbox, not who is asking: Corva trusts you for that, as it does for `phoneVerified`. Never call it for an anonymous visitor who typed a phone number, or anyone could confirm their own email on someone else's record. The answer is the same whether or not anyone matched. | Body field | Type | Description | | --- | --- | --- | | `customerId` | string | Corva's customer id (from `POST /accounts` or `POST /customers/verify`). Use this, or `claim`. | | `claim` | string | A phone number or email already on the customer's record. | | `email` **(required)** | string | The address to confirm. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/customers/email \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response: ```json { "sent": true, "note": "If that customer is on file, a confirmation email is on its way." } ``` #### `POST /api/v1/accounts`, Someone signed up or signed in to your own website or app Corva is not a login provider and does not need to be: keep whichever login you already use (your own, Firebase, Auth0, Clerk, Cognito, social sign-in) and, from **your server**, tell Corva when a person signs up, signs in or changes their details in your website or mobile app. Corva ties your user to the one customer it already holds (their calls, chats, enquiries, orders) or makes one, and answers with what it knows, so your app can say "welcome back" and everything they do next lands on the same record. **How the customer is found.** An account already linked is the customer. Otherwise a `phone` or `email` you mark `phoneVerified` / `emailVerified` (you sent the OTP or the link and they proved it) finds the customer it belongs to and joins their whole history. A number or email you do **not** mark verified never joins an existing customer, because typing a number is not proof: a new record is made and a match is left for the team (`needsReview`). Send `visitorId` (your first-party visitor id) and `sessionIds` (chats they had before signing in) to attach those anonymous conversations to them. `convertLeads: true` on `event: "signed_up"` marks their open leads Converted. Safe to call on every sign-in: it is idempotent per `(app, userId)`. Needs a key with the `website` or `write` scope; never call it from a browser. | Body field | Type | Description | | --- | --- | --- | | `userId` **(required)** | string | Your own id for this person (up to 128 characters; never shown to customers). May be left out when `profile.sub` carries it. | | `customerId` | string | Corva's own customer id, when your sign-in was proven by `POST /customers/verify` (its code) and you got the id back. The account is linked to exactly that customer. Use with a stable `userId` (it may be the same id). | | `idToken` | string | The ID token (JWT) your login issued for this person. Verified against the issuer set up in Settings → Your login; it **replaces** everything claimed in the body: `userId` is its `sub`, and the phone and email are verified only when the token's own `phone_number_verified` / `email_verified` say so. This is the recommended way: nothing is taken on a field's word. A business can require it (Settings → Your login). | | `profile` | object | Whatever your own login already knows about them, in your field names or the standard OIDC claims (`sub`, `name`, `given_name`, `family_name`, `email`, `email_verified`, `phone_number`, `phone_number_verified`, `address`…). Corva maps the ones it knows (name, phone, email, address, the two verified claims), keeps the rest as text on the account, and drops anything that looks like a secret (password, token, otp, card…). Fields you send explicitly win. | | `app` | string | Which of your products: "app" (default), "web", "ios"… 1–30 letters, digits, . _ -. One customer can have several. | | `event` | string | "signed_up", "signed_in" (default) or "updated" (details changed; not counted as a sign-in). | | `name` | string | Their name, used only to fill a record that has none. | | `phone` | string | Their phone number, any common format. | | `email` | string | Their email address. | | `phoneVerified` | boolean | true only when your system proved they hold this number (an OTP you sent). | | `emailVerified` | boolean | true only when your system proved they hold this address (a link you sent). | | `visitorId` | string | The visitor id you send to POST /visits, so the anonymous browser and its chats join the account. | | `sessionIds` | array | Up to 10 chat `sessionId`s they used before signing in. | | `convertLeads` | boolean | With event "signed_up": mark their open leads Converted. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/accounts \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_8f31", "app": "app", "event": "signed_up", "name": "Asha Verma", "phone": "+91 98111 20456", "phoneVerified": true, "email": "asha@example.com", "visitorId": "v_7d2a91c4e0", "sessionIds": [ "c4f1e2a9b7" ], "convertLeads": true }' ``` Response: ```json { "customer": { "id": "5b0e…", "name": "Asha Verma", "phone": "+91 98111 20456", "email": "asha@example.com", "segment": "App user", "customerSince": "2026-09-12T10:02:11.000Z", "externalRef": null }, "account": { "app": "app", "userId": "usr_8f31", "signedUpAt": "2026-10-12T09:14:00.000Z", "lastSeenAt": "2026-10-12T09:14:00.000Z", "signIns": 1, "closed": false }, "created": false, "linked": true, "matchedBy": "phone", "returning": true, "known": { "conversations": 3, "leads": 1, "openLeads": 1, "orders": 2, "lastConversationAt": "2026-10-09T16:40:00.000Z" }, "attached": { "visitor": true, "conversations": 1 }, "leadsConverted": 1, "needsReview": 0 } ``` #### `POST /api/v1/accounts/batch`, Link the users you already have The backfill for a business that adds Corva after it has users: up to 100 accounts a call, each linked to the customer Corva already holds by the same rules as a sign-in (a number or email you mark verified joins the history; anything else is made a new customer with a match left for the team). No sign-in is counted and no webhook is sent. `signedUpAt` keeps their real sign-up date. Safe to run again. | Body field | Type | Description | | --- | --- | --- | | `accounts` **(required)** | array | Each: `{ userId, app?, name?, phone?, email?, phoneVerified?, emailVerified?, signedUpAt?, profile? }`. A token is not used in an import. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/accounts/batch \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accounts": [ { "userId": "usr_1", "name": "Asha Verma", "phone": "+91 98111 20456", "phoneVerified": true, "signedUpAt": "2025-02-11T09:00:00Z" } ] }' ``` Response: ```json { "processed": 1, "createdCustomers": 0, "linkedToExisting": 1, "failed": 0, "results": [ { "index": 0, "userId": "usr_1", "ok": true, "customerId": "5b0e…", "created": false, "returning": true, "matchedBy": "phone" } ] } ``` #### `GET /api/v1/accounts`, One of your users, as Corva sees them The unified view of a user you registered: their customer, every sign-in linked to it (app and website), what Corva knows (conversations, leads, orders), their open leads and their latest order records. For your own screens; call it from your server. | Query parameter | Type | Description | | --- | --- | --- | | `userId` **(required)** | string | Your id for the person. | | `app` | string | Default "app". | Request: ```bash curl https://corva.tiruvi.site/api/v1/accounts?userId=…&app=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "customer": { "id": "5b0e…", "name": "Asha Verma", "phone": "+91 98111 20456", "email": "asha@example.com", "segment": "App user", "customerSince": "2026-09-12T10:02:11.000Z", "externalRef": null }, "account": { "app": "app", "userId": "usr_8f31", "signedUpAt": "2026-10-12T09:14:00.000Z", "lastSeenAt": "2026-10-12T09:14:00.000Z", "signIns": 4, "closed": false }, "accounts": [], "known": { "conversations": 3, "leads": 1, "openLeads": 0, "orders": 2, "lastConversationAt": "2026-10-09T16:40:00.000Z" }, "openLeads": [], "recentRecords": [ { "reference": "TD-1042", "kind": "order", "status": "Ready for delivery", "label": "Wash and fold", "amountRupees": 480 } ] } ``` #### `DELETE /api/v1/accounts`, The sign-in was closed Say a user closed or deleted their account with you. The sign-in stops identifying anyone in chats; the customer and their history stay (a person who is still a customer of the business). To erase a person's data, use the business's data-request process. | Query parameter | Type | Description | | --- | --- | --- | | `userId` **(required)** | string | Your id for the person. | | `app` | string | Default "app". | Request: ```bash curl -X DELETE https://corva.tiruvi.site/api/v1/accounts \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "closed": true, "account": { "app": "app", "userId": "usr_8f31", "signedUpAt": "2026-10-12T09:14:00.000Z", "lastSeenAt": "2026-10-12T09:14:00.000Z", "signIns": 4, "closed": true } } ``` #### `GET /api/v1/customers/{id}/addresses`, A customer's saved addresses Home, Work and any others the customer saved, the default first. The default is also what the customer's `location`, PIN code and exact point show, and what a booking is sent with. Request: ```bash curl https://corva.tiruvi.site/api/v1/customers/{id}/addresses \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "addresses": [ { "id": "a1b2…", "label": "Home", "address": "B-204, Palm Court, Sector 57, Gurugram", "pincode": "122003", "location": { "lat": 28.4089, "lng": 77.0507, "accuracyM": 18 }, "source": "gps", "isDefault": true } ] } ``` #### `POST /api/v1/customers/{id}/addresses`, Save an address Saves an address under a label; the same label again replaces it. The customer's first address becomes their default; send `makeDefault: true` to switch. Up to 10 per customer. The response says whether the PIN code is one the business serves. | Body field | Type | Description | | --- | --- | --- | | `label` **(required)** | string | Home, Work, or a name of their own. | | `address` **(required)** | string | The full address. | | `pincode` | string | Six digits. | | `lat` | number | Exact point, with lng. | | `lng` | number | Exact point, with lat. | | `accuracyM` | number | Accuracy in metres. | | `source` | string | typed (default), gps, pin, whatsapp_pin or map_link. | | `makeDefault` | boolean | Use this one from now on. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/customers/{id}/addresses \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "label": "Work", "address": "Tower B, Cyber Hub, DLF Phase 3, Gurugram", "pincode": "122002", "lat": 28.495, "lng": 77.089, "source": "pin" }' ``` Response: ```json { "address": { "id": "c3d4…", "label": "Work", "address": "Tower B, Cyber Hub, DLF Phase 3, Gurugram", "pincode": "122002", "location": { "lat": 28.495, "lng": 77.089, "accuracyM": null }, "source": "pin", "isDefault": false }, "served": true, "message": "We serve PIN code 122002, from our Sector 56 store." } ``` #### `PATCH /api/v1/customers/{id}/addresses/{addressId}`, Make an address the default The customer's record, PIN code and store follow it. Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/customers/{id}/addresses/{addressId} \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response: ```json { "address": { "id": "c3d4…", "label": "Work", "isDefault": true } } ``` #### `DELETE /api/v1/customers/{id}/addresses/{addressId}`, Remove a saved address If it was the default, the oldest remaining address takes its place. Request: ```bash curl -X DELETE https://corva.tiruvi.site/api/v1/customers/{id}/addresses/{addressId} \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "removed": true } ``` #### `GET /api/v1/customers/{id}`, One customer A customer with everything collected about them (`details`: the newest answer to each of the business's fields, across all their leads), their leads and their conversations. Request: ```bash curl https://corva.tiruvi.site/api/v1/customers/{id} \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70", "owner": "Kavya Rao", "createdAt": "2026-09-12T10:02:11.000Z" }, "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "leads": [ { "id": "c505…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "interest": "AC service · visit 2026-10-04, 8–10 AM · B-402, Palm Grove, Sector 70", "notes": "Ref AH-7K3QX9", "stage": "new", "stageLabel": "New", "valueRupees": null, "source": "web_chat", "createdByAi": true, "owner": "Kavya Rao", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" }, "customerConfirmedAt": "2026-10-03T11:12:00.000Z", "customerId": "a734…", "conversationId": "0b7c…", "createdAt": "2026-10-03T04:31:12.000Z", "updatedAt": "2026-10-03T04:31:12.000Z" } ], "conversations": [ { "id": "0b7c…", "channel": "chat", "status": "resolved", "outcome": "ai_resolved", "intent": "Book an AC service", "summary": "Booked an AC service visit for Saturday morning.", "handledBy": null, "isTest": false, "customerId": "a734…", "sessionId": "chat_8c1f2a", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service" }, "startedAt": "2026-10-03T04:28:40.000Z", "endedAt": "2026-10-03T04:31:55.000Z", "durationSeconds": 195 } ] } ``` #### `POST /api/v1/customers/verify`, Prove a customer at the counter For a connected system's staff screen. `{ claim }` (a phone, an email or an order reference) sends a 6-digit code to the contact the business already has for that customer, never to one typed now; the answer is the same whether or not anyone matched. `{ claim, code }` checks what the customer read out: a right code returns the customer, their details, leads and conversations (the shape of `GET /customers/{id}`); a wrong one returns `verified: false`. Ten minutes, five tries, six codes per contact a day. | Body field | Type | Description | | --- | --- | --- | | `claim` **(required)** | string | A phone number, an email or an order reference. | | `code` | string | The 6 digits the customer read out. Leave out to send a code. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/customers/verify \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response: ```json { "verified": true, "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70", "owner": "Kavya Rao", "createdAt": "2026-09-12T10:02:11.000Z" }, "details": {}, "leads": [], "conversations": [] } ``` #### `GET /api/v1/conversations`, List chats and calls Conversations, newest first, each with the `details` it collected. The owner's own test conversations are left out unless you ask for them. | Query parameter | Type | Description | | --- | --- | --- | | `channel` | string | "chat" or "voice". | | `since` | string | Started at or after this time. | | `sessionId` | string | The chat your site started under this sessionId. | | `customerId` | string | One customer's conversations. | | `details.` | string | A detail collected in the conversation equals this. | | `includeTests` | string | "true" to include conversations from Try it. | | `limit` | number | 1–100, default 25. | | `cursor` | string | `nextCursor` from the previous page. | Request: ```bash curl https://corva.tiruvi.site/api/v1/conversations?channel=…&since=…&sessionId=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "conversations": [ { "id": "0b7c…", "channel": "chat", "status": "resolved", "outcome": "ai_resolved", "intent": "Book an AC service", "summary": "Booked an AC service visit for Saturday morning.", "handledBy": null, "isTest": false, "customerId": "a734…", "sessionId": "chat_8c1f2a", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service" }, "startedAt": "2026-10-03T04:28:40.000Z", "endedAt": "2026-10-03T04:31:55.000Z", "durationSeconds": 195 } ], "nextCursor": null } ``` #### `GET /api/v1/conversations/{id}`, One conversation, with its transcript `from` is "customer", "assistant", "system" (an event: a takeover, the call ending) or the name of the person on the team who spoke. Request: ```bash curl https://corva.tiruvi.site/api/v1/conversations/{id} \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "conversation": { "id": "0b7c…", "channel": "chat", "status": "resolved", "outcome": "ai_resolved", "intent": "Book an AC service", "summary": "Booked an AC service visit for Saturday morning.", "handledBy": null, "isTest": false, "customerId": "a734…", "sessionId": "chat_8c1f2a", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service" }, "startedAt": "2026-10-03T04:28:40.000Z", "endedAt": "2026-10-03T04:31:55.000Z", "durationSeconds": 195 }, "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "address": "B-402, Palm Grove, Sector 70" }, "transcript": [ { "ordinal": 0, "from": "customer", "text": "My AC is leaking. Can someone come tomorrow morning?", "atSeconds": 0 }, { "ordinal": 1, "from": "assistant", "text": "Please check the details below and tap Confirm.", "atSeconds": 4 } ] } ``` ### Payments, Tell Corva where a payment to the business stands, so the team and the assistant know it was paid. #### `POST /api/v1/payments`, Tell Corva where a payment stands Your system collects the money (your own payment account, Corva never holds the keys). Send each payment's state here whenever it changes: when the link is made, when it is paid, when it expires. The same `reference` replaces what was there, and a status never moves backwards, so a late `expired` cannot undo a `paid`. When it becomes `paid`, the conversation it was asked in says so, on WhatsApp the customer is thanked, and the lead it was for (that conversation's, else the customer's open one) moves to Converted. Echo `requestId` and `conversationId` when Corva asked for the link (see Payments below). | Body field | Type | Description | | --- | --- | --- | | `reference` **(required)** | string | The payment's id in your system. | | `status` | string | pending (default), partially_paid, paid, expired, cancelled or failed. | | `amountRupees` | number | What was asked for. Required the first time. | | `amountPaidRupees` | number | What has been paid so far. | | `description` | string | What it is for, as the customer sees it. | | `url` | string | The link the customer pays at. | | `pageUrl` | string | Your own page for it, if any. | | `qrUrl` | string | An image of a QR for the link, if any. | | `method` | string | upi, card, netbanking … once paid. | | `paidAt` | string | When it was paid; defaults to when Corva hears. | | `expiresAt` | string | When the link stops working. | | `orderReference` | string | The order it pays for. | | `requestId` | string | Corva's id for the request, when Corva asked. | | `conversationId` | string | The conversation it was asked in, when Corva asked. | | `customer` | object | { id } or { phone, name?, email? }, when Corva did not ask. | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/payments \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reference": "PAY-7K3QX9", "status": "paid", "amountRupees": 150, "amountPaidRupees": 150, "method": "upi", "orderReference": "AH-7K3QX9", "requestId": "cpr_5c1f…" }' ``` Response: ```json { "payment": { "id": "9b2d…", "reference": "PAY-7K3QX9", "status": "paid", "amountRupees": 150, "amountPaidRupees": 150, "description": "Order AH-7K3QX9, AC service", "url": "https://rzp.io/rzp/abc123", "pageUrl": "https://your-site.com/pay/PAY-7K3QX9", "qrUrl": "https://your-site.com/pay/PAY-7K3QX9/qr", "method": "upi", "orderReference": "AH-7K3QX9", "customerId": "c1d2…", "conversationId": "3e4f…", "requestedBy": "Ava (AI)", "paidAt": "2026-10-03T09:15:00.000Z", "expiresAt": "2026-10-10T09:10:00.000Z", "createdAt": "2026-10-03T09:10:00.000Z" }, "created": false } ``` #### `GET /api/v1/payments`, Payments Corva holds By your `reference`, or every payment of one customer, newest first. | Query parameter | Type | Description | | --- | --- | --- | | `reference` | string | Your payment's reference. | | `customerId` | string | A customer's id. | Request: ```bash curl https://corva.tiruvi.site/api/v1/payments?reference=…&customerId=… \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "payments": [ { "id": "9b2d…", "reference": "PAY-7K3QX9", "status": "paid", "amountRupees": 150, "amountPaidRupees": 150, "description": "Order AH-7K3QX9, AC service", "url": "https://rzp.io/rzp/abc123", "pageUrl": "https://your-site.com/pay/PAY-7K3QX9", "qrUrl": "https://your-site.com/pay/PAY-7K3QX9/qr", "method": "upi", "orderReference": "AH-7K3QX9", "customerId": "c1d2…", "conversationId": "3e4f…", "requestedBy": "Ava (AI)", "paidAt": "2026-10-03T09:15:00.000Z", "expiresAt": "2026-10-10T09:10:00.000Z", "createdAt": "2026-10-03T09:10:00.000Z" } ] } ``` ### Webhooks, Register the addresses Corva sends events to, without opening the console. #### `GET /api/v1/webhooks`, The webhooks the business has Each address Corva sends events to, with the events it gets and how its last delivery went. Secrets are never listed: one is shown once, when the webhook is registered. Request: ```bash curl https://corva.tiruvi.site/api/v1/webhooks \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "webhooks": [ { "id": "5d0e…", "url": "https://your-site.com/api/corva-webhook", "events": [ "lead.created", "lead.updated", "follow_up.created", "handoff.requested", "conversation.ended" ], "createdBy": "API", "createdAt": "2026-10-03T04:00:00.000Z", "lastDelivery": { "at": "2026-10-03T04:31:13.000Z", "status": 200, "error": null } } ] } ``` #### `POST /api/v1/webhooks`, Register a webhook Register an address for events without opening the console, so a system you connect can set up its own. The answer carries the signing secret, **once**, keep it to verify `Corva-Signature` on each delivery (see Webhooks above). Registering an address the business already has keeps one webhook: it gets a new secret and the events you send. Five webhooks at most; the address must be public and https. | Body field | Type | Description | | --- | --- | --- | | `url` **(required)** | string | Where to POST events. https, and reachable from the internet. | | `events` | string[] | Which events to send. Omit for all of them. | | `name` | string | Who is registering it, shown beside it in Settings, "Orbo", your system's name. Default "API". | Request: ```bash curl -X POST https://corva.tiruvi.site/api/v1/webhooks \ -H "Authorization: Bearer $CORVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-site.com/api/corva-webhook", "events": [ "lead.created", "conversation.ended" ], "name": "Our CRM" }' ``` Response: ```json { "id": "5d0e…", "url": "https://your-site.com/api/corva-webhook", "events": [ "lead.created", "conversation.ended" ], "createdBy": "Our CRM", "createdAt": "2026-10-03T04:00:00.000Z", "lastDelivery": null, "secret": "whsec_…", "created": true } ``` #### `DELETE /api/v1/webhooks/{id}`, Remove a webhook Stop sending events to an address. Anything of yours that depended on them stops hearing about changes. Request: ```bash curl -X DELETE https://corva.tiruvi.site/api/v1/webhooks/{id} \ -H "Authorization: Bearer $CORVA_API_KEY" ``` Response: ```json { "removed": true } ``` ### Visitors, Who is on the site, with their consent. #### `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, so the team sees that this lead came from an Instagram ad. Always send the consent your banner actually recorded. | Body field | Type | Description | | --- | --- | --- | | `visitorId` **(required)** | string | Your first-party visitor id (8–80 chars). | | `consent` **(required)** | "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". | Request: ```bash 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" } }' ``` Response: ```json { "recorded": true } ``` ### Voice, Talk to the assistant from the browser. #### `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 start one call. The protocol is under “Voice calls in the browser”. | Body field | Type | Description | | --- | --- | --- | | `visitorId` | string | Your visitor id. | | `name` | string | Their name, if known, the assistant greets them by it. | | `phone` | string | Their number, if known, joins the call to their record. | Request: ```bash 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" }' ``` Response: ```json { "token": "eyJicmFuZElkIjoi…", "expiresInSeconds": 300, "bridgeUrl": "wss://app.example.com/api/voice", "agentName": "Ava" } ``` ## 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. Calls are push-to-talk: the visitor holds a button (or the space bar) while speaking and lets go to hear the answer. The conversation, the details collected and anything booked land in the business's console like a phone call. ```text → connect new WebSocket(bridgeUrl) → send {"type":"start","token":"…"} ← receive {"type":"ready","agent":"Ava","capSeconds":180, …} while the visitor holds "talk": → send binary frames, PCM16, mono, 16 kHz when they let go: → send {"type":"end_turn"} ← receive binary frames, PCM16, mono, 24 kHz: play them in order ← receive {"type":"heard","text":"…"} what the visitor said, so far ← receive {"type":"said","text":"…"} what the assistant said, so far ← receive {"type":"turn_complete"} a person on the team takes the call: ← receive {"type":"held","by":"Kavya Rao"} drop any assistant audio still queued the assistant says one line handing over; then the person's voice arrives as the same 24 kHz binary frames, keep playing them ← receive {"type":"human","name":"…","text":"…"} what they said or typed, as a caption ← receive {"type":"released"} handed back to the assistant ← receive {"type":"error","message":"…"} show it; the call is over ← receive {"type":"closed","reason":"…","seconds":142} → 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`); hide the call button when it is false. **Limits.** An unattended call with the assistant ends at `capSeconds`; a call a person has taken over can run longer. A call with no audio from the visitor for 45 seconds is closed. ## Webhooks The API is your site talking to Corva; a webhook is Corva telling your systems what just happened, to put a new lead into your own CRM or order system, post it to a team channel, or start a job. The business adds a URL in Corva → **Settings → Webhooks**, chooses the events, and is shown a signing secret (`whsec_…`) once. A system can do the same with its key, `POST /api/v1/webhooks` answers with the secret, once, which is how Orbo connects without anything being pasted. | Event | When | | --- | --- | | `lead.created` | A new lead, from a call, a chat, your website's form, or added by hand. | | `lead.updated` | A lead changed: new details, a new stage, a new owner. | | `lead.converted` | A lead was converted: everything gathered about the customer and what they asked for, in one payload, so your own system can place the order. Sent a minute after a person converts it, unless they confirm sooner or put it back. | | `follow_up.created` | Someone on the team now owes the customer something, by a time. | | `handoff.requested` | The AI needs a person: it reached a limit, or the customer asked for one. | | `conversation.ended` | A call or chat finished, with its outcome and the details collected. | | `customer.created` | A customer Corva did not have before: from an account your app registered, a call, a chat or an email. | | `customer.erased` | The business erased a customer at their request. Only the ids: erase the person on your side too. | | `customer.linked` | Your app's sign-in was tied to a customer Corva already knew: someone who had called, chatted or enquired before. | Each delivery is one `POST` with a JSON body and two headers: `Corva-Event` (the type) and `Corva-Signature`. The URL must be `https` on a public address and answer itself, a redirect is not followed. Answer `2xx` within six seconds. A `5xx` or no answer is retried once; treat webhooks as notifications and the API as the record, and be ready to receive the same event twice (`id` is unique per event). ```json { "id": "evt_3f9c0a1b2c3d4e5f6a7b8c9d", "type": "lead.created", "createdAt": "2026-10-03T04:31:13.000Z", "data": { "lead": { "id": "c505…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com", "interest": "AC service · visit 2026-10-04, 8–10 AM · B-402, Palm Grove, Sector 70", "notes": "Ref AH-7K3QX9", "stage": "new", "stageLabel": "New", "valueRupees": null, "source": "web_chat", "createdByAi": true, "owner": "Kavya Rao", "details": { "address": "B-402, Palm Grove, Sector 70", "service_type": "AC service", "units": "2 split ACs" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" }, "customerConfirmedAt": "2026-10-03T11:12:00.000Z", "customerId": "a734…", "conversationId": "0b7c…", "createdAt": "2026-10-03T04:31:12.000Z", "updatedAt": "2026-10-03T04:31:12.000Z" }, "customer": { "id": "a734…", "name": "Riya Sharma", "phone": "+91 98765 43210", "email": "riya@example.com" }, "request": { "kind": "booking", "reference": "AH-7K3QX9", "services": [ "AC service" ], "address": "B-402, Palm Grove, Sector 70", "date": "2026-10-04", "timeSlot": "8–10 AM" } } } ``` **Verify every delivery.** The signature is an HMAC-SHA256 of `"."` with your secret, hex-encoded. Use the raw request body, exactly as received, and reject a timestamp more than five minutes old. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; // Next.js route handler: app/api/corva-webhook/route.ts export async function POST(req: Request) { const body = await req.text(); // raw, before any JSON.parse const header = req.headers.get("corva-signature") ?? ""; const t = header.match(/t=(\d+)/)?.[1]; const v1 = header.match(/v1=([0-9a-f]+)/)?.[1]; if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return new Response("stale", { status: 400 }); const expected = createHmac("sha256", process.env.CORVA_WEBHOOK_SECRET!).update(`${t}.${body}`).digest("hex"); if (expected.length !== v1.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) { return new Response("bad signature", { status: 401 }); } const event = JSON.parse(body); if (event.type === "lead.created") await addToOurSystem(event.data.lead, event.data.customer); return new Response("ok"); } ``` `Send test` in Settings posts a `ping` event to your URL and shows what it answered. The other payloads, `lead.updated`, `lead.converted`, `follow_up.created`, `handoff.requested`, `conversation.ended`, are in the [OpenAPI spec](https://corva.tiruvi.site/api/v1/openapi.json) under `webhooks`. **Placing the order in your own system.** Corva keeps customers, conversations and leads, not orders (Orbo does, when it is on). Register your URL for `lead.converted`: it is sent once when a lead is converted, with the customer, what they asked for (`order.items`), the address and map pin, the slot, the notes, what was paid and the lead's own details, so your system can make the order. A person's move to Converted or Lost is held for a minute (they can put it back or confirm sooner); a payment reported paid is sent at once. Orbo keeps working from `lead.updated` and is not affected. ## Taking payments The team in the console, and the assistant on a chat, WhatsApp or a call, can ask a customer to pay. The money is the business's: **your** system makes the payment link with **your** payment account (Razorpay, or any other), so no payment key ever reaches Corva. The business sets your endpoint in Corva → **Settings → Payments** and is shown a signing secret (`cps_…`) once. When a payment is asked for, Corva sends your endpoint one `POST`, signed exactly like a webhook (`Corva-Signature`, verified the same way as above, with this secret). Answer within 15 seconds with the payment as you made it, the same fields you would send to `POST /api/v1/payments`. The assistant only ever sends an `orderReference`: it never names an amount, so work out what is owed from your own order. A person in the console may send `amountRupees`. If you cannot make a link, answer `4xx` with `{ "error": "…" }` in words the customer may be told ("There is no bill on this order yet"). The same `requestId` twice must return the same link. Discounts never come from the model. A business publishes its **offers** in Corva (a code, a percentage with a cap or a flat amount, a minimum order, dates, first order only, once per customer); when a customer names one, Corva checks it in code and, only if it applies, sends it as `offer`. Take it off the amount you work out, `value` is a percentage when `kind` is `"percent"`, rupees when it is `"flat"`, or answer `4xx` if your order does not qualify. No `offer`, no discount. Before your endpoint is called for the assistant, two checks happen in Corva, both on by default (Settings → Payments): the customer proves who they are with a code, and a person on the team approves the request. So a request may reach you some minutes after the customer asked, with `requestedBy` naming who approved it. ```json { "requestId": "cpr_5c1f9a0b3d2e4f6a7b8c9d0e", "orderReference": "AH-7K3QX9", "customer": { "id": "c1d2…", "name": "Riya Sharma", "phone": "+91 98765 43210" }, "conversationId": "3e4f…", "requestedBy": "Ava (AI), approved by Kavya Rao", "notify": true, "offer": { "code": "FIRST20", "title": "20% off your first order", "kind": "percent", "value": 20, "maxDiscountRupees": 200, "minOrderRupees": 500 } } ``` Then, whenever it changes, above all when your payment provider says it is paid, send its state to `POST /api/v1/payments`. Corva shows it on the customer's record and in the conversation, thanks the customer on WhatsApp, and the assistant can answer "has my payment gone through?". ## 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. ## Going live - The key is only in your server's environment, and `/health` answers `ok` from production. - The site reads `/config` rather than hard-coding the assistant's name, fields or booking word. - A chat keeps one `sessionId`; a new chat gets a new one; `visitorId` goes on every call. - Cards lock the input until Confirm or Edit, and a `409` or `422` from confirm is shown, not swallowed. - Closing the chat or reloading the page sends `/chat/end`, and a `409` on the next message starts a new `sessionId`. - The chat polls `GET /chat` so a person taking over is seen, and shows their name. - The voice button is hidden unless `features.voice` (and `voiceSecure` on https) is true. - Your own rate limit (and ideally a bot check) sits in front of your chat route, the business's 300 requests a minute are shared by all your visitors, one chat may send 12 messages a minute, and every new `sessionId` is a new chat on the business's plan (120 may start an hour). Make session ids with a cryptographic random source. - Webhook deliveries are verified, and a repeat of the same `id` does nothing twice. - The business has checked its knowledge, its Details to collect and who on the team gets leads. Corva API v1 · additions are backwards-compatible within v1: expect new fields and new event types, and ignore what you do not know. Questions: ask whoever at the business gave you the key, or Corva support.