Skip to the guide
Corva API v1 · REST + webhooks

Put a business’s AI assistant on its website: and its work in your systems.

Corva runs an AI assistant for each business. It answers from the business’s own knowledge, collects what the business asks for, and turns every conversation into a customer, a lead with an owner and a follow-up. This API is how a website and the business’s systems plug into that.

Base URLhttps://corva.tiruvi.site
Building with an AI assistant?Give it the whole guide, or point it at corva.tiruvi.site/llms.txt and let it read for itself.

Quick start

A key, two settings, and a chat route on your server.

  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.
TypeScript
// 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, when a chat ends, one customer across channels, the leads board, details, data out, bookings, takeover.

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

Bearer keys, status codes, rate limits, timeouts and formats.

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

StatusMeaning
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.
403The key works but lacks the scope this route needs (website, read, write or webhooks; chosen when the key is made).
402The business's plan has no room for a new conversation. error is safe to show a customer; treat the assistant as offline.
404No conversation or card with that id.
409The chat has ended (start a new sessionId), or a card was answered or replaced already.
413The body is over 64 KB.
422A card can no longer be confirmed as it is, e.g. its date has passed. Show error and let them edit.
429Over 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.
5xxOur 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

Every endpoint, with its fields, a request and a response.

Base URL https://corva.tiruvi.site. Every request carries Authorization: Bearer ck_…. Example values are from a made-up home-services business ("Acme Home Services", assistant "Ava"); a real business's fields and words come from /config.

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

curl https://corva.tiruvi.site/api/v1/health \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/config \
  -H "Authorization: Bearer $CORVA_API_KEY"

ChatThe 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
sessionIdstringrequired

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.

messagestringrequired

What the customer typed. Up to 2,000 characters.

visitorIdstring

Your first-party visitor id, to join the chat to their visits and later calls.

customerobject

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

streamboolean

Answer as server-sent events, to show the reply as it is written.

curl -X POST https://corva.tiruvi.site/api/v1/chat \
  -H "Authorization: Bearer $CORVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "sessionId": "chat_8c1f2a",
  "message": "My AC is leaking. Can someone come tomorrow morning? I'm Riya, 98765 43210, B-402 Palm Grove, Sector 70",
  "visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d"
}'
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
sessionIdstringrequired

The chat's sessionId.

latnumberrequired

Latitude.

lngnumberrequired

Longitude.

consentbooleanrequired

True: the visitor agreed to share it.

sourcestring

gps (the browser's location) or pin (dropped on a map). Default pin.

accuracyMnumber

The browser's accuracy in metres, for gps.

addressstring

The readable address for the point, if your map gave one.

pincodestring

Its six-digit PIN code, if known.

saveAsstring

Save as a named address: Home, Work. Needs address.

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"
}'
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
sessionIdstringrequired

The chat's sessionId.

proposalIdstringrequired

proposal.id from the chat reply.

approvedbooleanrequired

true for Confirm, false for Edit.

visitorIdstring

Your visitor id, to join the booking to their visits.

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"
}'
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 parameters
sessionIdstringrequired

The chat's sessionId.

afternumber

Only messages after this ordinal. Start at -1, then pass the highest ordinal you have seen.

curl https://corva.tiruvi.site/api/v1/chat?sessionId=…&after=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
sessionIdstringrequired

The chat's sessionId.

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"
}'
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
sessionIdstringrequired

Your chat id.

visitorIdstring

Your visitor id.

messages{ role: "user" | "assistant", text: string }[]required

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 service window ACs?"
    },
    {
      "role": "assistant",
      "text": "Yes, window and split units both."
    }
  ]
}'

LeadsBookings, 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 keys 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
kind"booking" | "callback" | "enquiry"required

What they asked for. ("pickup" is accepted as another word for "booking".)

namestringrequired

Their name.

phonestringrequired

Their mobile, in any common Indian format.

emailstring

Where the confirmation goes. Strongly recommended.

referencestring

Your reference for this request, also the idempotency key. Corva makes one if you do not.

detailsobject

The business's Details to collect: { key: value }, keys from GET /config.

servicesstring[]

What they want done, for a booking.

addressstring

Where, when the booking needs an address.

pincodestring

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.

datestring

For a booking: YYYY-MM-DD.

timeSlotstring

For a booking, e.g. "8–10 AM".

preferredTimestring

For a callback, e.g. "today after 6 PM".

topicstring

What they want to talk about.

notesstring

Anything else.

promoCodestring

An offer code they used.

visitorIdstring

Your visitor id, to join the request to their visits.

sessionIdstring

The chat it came from, if your own assistant took it (see /chats).

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"
}'

RecordsRead 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.<key>=<value>, keys from /config, matched case-insensitively; details.<key>= 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 parameters
stagestring

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.

sincestring

Created at or after this time (ISO 8601).

updatedSincestring

Changed at or after this time, what a sync asks for.

details.<key>string

A collected detail equals this, e.g. details.service_type=AC service. Repeat for several.

phonestring

The lead's phone number, any format.

referencestring

The reference a booking or callback was given.

qstring

Text in the name, the request or the notes.

limitnumber

1–100, default 25.

cursorstring

nextCursor from the previous page.

curl https://corva.tiruvi.site/api/v1/leads?stage=…&since=…&updatedSince=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/leads/{id} \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
stagestring

new, contacted, proposal (Processing), won (Converted) or lost. qualified is accepted as proposal.

lostReasonstring

Why, when the stage is lost.

notesstring

Replaces the lead's notes.

valueRupeesnumber

What the lead is worth, in rupees; null to clear.

detailsobject

{ key: value }, merged into the lead's details; keys from GET /config.

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"
  }
}'
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
referencestringrequired

The id the customer was given.

labelstringrequired

What it is, in a few words.

statusstring

Where it has got to, as a sentence the customer can be told.

kindstring

order (default), booking, delivery, ticket, invoice or subscription.

amountRupeesnumber

The bill, when there is one.

leadIdstring

The lead it grew out of.

customerobject

{ id } or { phone, name?, email? }, found by phone, or created.

metaobject

Anything else worth knowing, as fields: stage, expected times, driver. The assistant may use it; never put an address or phone number here.

occurredAtstring

When the record began; defaults to now.

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"
  }
}'
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 parameters
referencestring

The reference, in any spacing or case.

curl https://corva.tiruvi.site/api/v1/records?reference=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
keystringrequired

Your own id for this message, such as "TD-7K3QX9:ready". Posting it again sends nothing.

kindstringrequired

pickup, started, ready, payment_due or bill.

textstringrequired

The message, as the customer should read it (600 characters at most).

referencestring

The order reference, shown in the email's subject.

linkstring

An https address: the tracking page, or the payment link for payment_due. Added to the message.

linkLabelstring

The email button's words (30 characters), such as "Pay now"; defaults to one that fits the kind.

customerobject

{ id } or { phone, email?, name? }.

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"
  }
}'
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 parameters
daysnumber

How many days back, 1–90. Default 7.

curl https://corva.tiruvi.site/api/v1/summary?days=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/team \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/catalog \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
itemsarrayrequired

Each: { category?, name, price?, unit?, description?, available?, sku?, gstPercent?, hsn?, priceIncludesGst? }, the template's columns.

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
    }
  ]
}'
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 parameters
phonestring

Their phone number, any format.

emailstring

Their email address.

qstring

Text in the name, email or phone.

sincestring

Created at or after this time.

limitnumber

1–100, default 25.

cursorstring

nextCursor from the previous page.

curl https://corva.tiruvi.site/api/v1/customers?phone=…&email=…&q=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
customerIdstring

Corva's customer id (from POST /accounts or POST /customers/verify). Use this, or claim.

claimstring

A phone number or email already on the customer's record.

emailstringrequired

The address to confirm.

curl -X POST https://corva.tiruvi.site/api/v1/customers/email \
  -H "Authorization: Bearer $CORVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
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
userIdstringrequired

Your own id for this person (up to 128 characters; never shown to customers). May be left out when profile.sub carries it.

customerIdstring

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

idTokenstring

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

profileobject

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.

appstring

Which of your products: "app" (default), "web", "ios"… 1–30 letters, digits, . _ -. One customer can have several.

eventstring

"signed_up", "signed_in" (default) or "updated" (details changed; not counted as a sign-in).

namestring

Their name, used only to fill a record that has none.

phonestring

Their phone number, any common format.

emailstring

Their email address.

phoneVerifiedboolean

true only when your system proved they hold this number (an OTP you sent).

emailVerifiedboolean

true only when your system proved they hold this address (a link you sent).

visitorIdstring

The visitor id you send to POST /visits, so the anonymous browser and its chats join the account.

sessionIdsarray

Up to 10 chat sessionIds they used before signing in.

convertLeadsboolean

With event "signed_up": mark their open leads Converted.

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
}'
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
accountsarrayrequired

Each: { userId, app?, name?, phone?, email?, phoneVerified?, emailVerified?, signedUpAt?, profile? }. A token is not used in an import.

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"
    }
  ]
}'
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 parameters
userIdstringrequired

Your id for the person.

appstring

Default "app".

curl https://corva.tiruvi.site/api/v1/accounts?userId=…&app=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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 parameters
userIdstringrequired

Your id for the person.

appstring

Default "app".

curl -X DELETE https://corva.tiruvi.site/api/v1/accounts \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/customers/{id}/addresses \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
labelstringrequired

Home, Work, or a name of their own.

addressstringrequired

The full address.

pincodestring

Six digits.

latnumber

Exact point, with lng.

lngnumber

Exact point, with lat.

accuracyMnumber

Accuracy in metres.

sourcestring

typed (default), gps, pin, whatsapp_pin or map_link.

makeDefaultboolean

Use this one from now on.

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"
}'
PATCH/api/v1/customers/{id}/addresses/{addressId}
Make an address the default

The customer's record, PIN code and store follow it.

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 '{}'
DELETE/api/v1/customers/{id}/addresses/{addressId}
Remove a saved address

If it was the default, the oldest remaining address takes its place.

curl -X DELETE https://corva.tiruvi.site/api/v1/customers/{id}/addresses/{addressId} \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/customers/{id} \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
claimstringrequired

A phone number, an email or an order reference.

codestring

The 6 digits the customer read out. Leave out to send a code.

curl -X POST https://corva.tiruvi.site/api/v1/customers/verify \
  -H "Authorization: Bearer $CORVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
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 parameters
channelstring

"chat" or "voice".

sincestring

Started at or after this time.

sessionIdstring

The chat your site started under this sessionId.

customerIdstring

One customer's conversations.

details.<key>string

A detail collected in the conversation equals this.

includeTestsstring

"true" to include conversations from Try it.

limitnumber

1–100, default 25.

cursorstring

nextCursor from the previous page.

curl https://corva.tiruvi.site/api/v1/conversations?channel=…&since=…&sessionId=… \
  -H "Authorization: Bearer $CORVA_API_KEY"
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.

curl https://corva.tiruvi.site/api/v1/conversations/{id} \
  -H "Authorization: Bearer $CORVA_API_KEY"

PaymentsTell 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
referencestringrequired

The payment's id in your system.

statusstring

pending (default), partially_paid, paid, expired, cancelled or failed.

amountRupeesnumber

What was asked for. Required the first time.

amountPaidRupeesnumber

What has been paid so far.

descriptionstring

What it is for, as the customer sees it.

urlstring

The link the customer pays at.

pageUrlstring

Your own page for it, if any.

qrUrlstring

An image of a QR for the link, if any.

methodstring

upi, card, netbanking … once paid.

paidAtstring

When it was paid; defaults to when Corva hears.

expiresAtstring

When the link stops working.

orderReferencestring

The order it pays for.

requestIdstring

Corva's id for the request, when Corva asked.

conversationIdstring

The conversation it was asked in, when Corva asked.

customerobject

{ id } or { phone, name?, email? }, when Corva did not ask.

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…"
}'
GET/api/v1/payments
Payments Corva holds

By your reference, or every payment of one customer, newest first.

Query parameters
referencestring

Your payment's reference.

customerIdstring

A customer's id.

curl https://corva.tiruvi.site/api/v1/payments?reference=…&customerId=… \
  -H "Authorization: Bearer $CORVA_API_KEY"

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

curl https://corva.tiruvi.site/api/v1/webhooks \
  -H "Authorization: Bearer $CORVA_API_KEY"
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
urlstringrequired

Where to POST events. https, and reachable from the internet.

eventsstring[]

Which events to send. Omit for all of them.

namestring

Who is registering it, shown beside it in Settings, "Orbo", your system's name. Default "API".

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"
}'
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.

curl -X DELETE https://corva.tiruvi.site/api/v1/webhooks/{id} \
  -H "Authorization: Bearer $CORVA_API_KEY"

VisitorsWho 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
visitorIdstringrequired

Your first-party visitor id (8–80 chars).

consent"necessary" | "all"required

What they chose on your cookie banner.

typestring

"page_view" (default) or "consent".

pathstring

Only kept with consent "all".

referrerstring

Only kept with consent "all".

utmobject

utm_source, utm_medium, utm_campaign… Only kept with consent "all".

curl -X POST https://corva.tiruvi.site/api/v1/visits \
  -H "Authorization: Bearer $CORVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
  "consent": "all",
  "type": "page_view",
  "path": "/",
  "referrer": "https://instagram.com",
  "utm": {
    "utm_source": "instagram"
  }
}'

VoiceTalk 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
visitorIdstring

Your visitor id.

namestring

Their name, if known, the assistant greets them by it.

phonestring

Their number, if known, joins the call to their record.

curl -X POST https://corva.tiruvi.site/api/v1/voice-sessions \
  -H "Authorization: Bearer $CORVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "visitorId": "v_3f9c0a1b2c3d4e5f6a7b8c9d",
  "name": "Riya Sharma",
  "phone": "9876543210"
}'

Voice calls in the browser

Tokens, the WebSocket protocol, iPhone gotchas and limits.

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.

Protocol
→ 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

Events Corva sends your systems, and how to verify each delivery.

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.

EventWhen
lead.createdA new lead, from a call, a chat, your website's form, or added by hand.
lead.updatedA lead changed: new details, a new stage, a new owner.
lead.convertedA 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.createdSomeone on the team now owes the customer something, by a time.
handoff.requestedThe AI needs a person: it reached a limit, or the customer asked for one.
conversation.endedA call or chat finished, with its outcome and the details collected.
customer.createdA customer Corva did not have before: from an account your app registered, a call, a chat or an email.
customer.erasedThe business erased a customer at their request. Only the ids: erase the person on your side too.
customer.linkedYour 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 "<t>.<raw body>" with your secret, hex-encoded. Use the raw request body, exactly as received, and reject a timestamp more than five minutes old.

TypeScript
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 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

Your payment endpoint, offers checked in code, and reporting a payment back.

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

The two first-party cookies we suggest, and what consent changes.

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 checklist before real customers use it.

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

Orders, deliveries & bills

Orders, deliveries, bills, payments and stock for the same business: place an order, move it along, bill it, ask for payment, read stock.

Base URL https://orbo.tiruvi.site/api/v1, a separate host from the rest of this reference. Every request carries Authorization: Bearer ok_live_…, a key for the business's orders and bills, made in the Developers page of the console. Money is in paise (₹1 = 100 paise) unless a field says rupees. Times are ISO 8601. Errors are { "error": { "message": … } }.

The same description for tools: https://orbo.tiruvi.site/api/v1/openapi.json.

OrdersOrders through your own stages, a tracking page for each customer, updates as orders move, and an orders API.

POST/orders
Place an order
Parameters
Idempotency-Keyheader

8–100 letters, digits, dashes or underscores. A retried request with the same key returns the first answer instead of doing the thing twice. Kept a day.

Body
referencestring

The brand's own reference, if it gave the customer one. 2–40 letters, digits, dashes.

customerobjectrequired
itemsarray
storestring

The store's code. Without one, the nearest store to the address.

stagestring
addressstring
latnumber
lngnumber
notesstring
expressboolean

A driver straight to the customer, ahead of the other stops. Refused at a store that does not offer it.

pickupobject
deliveryobject
externalRefstring
sourcestring
Answers
201

The order, with its tracking link.

400

Something in the order cannot be accepted (an unknown SKU, a store that does not offer express…).

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

409

An order with that reference exists already.

GET/orders
List orders
Parameters
openquery

1 for orders not yet delivered or cancelled.

phasequery

One phase.

qquery

Matches the reference, the customer's name or phone.

limitquery

1–200, default 50.

includequery

stops adds each order's pickup and delivery stops.

trackingquery

The code at the end of a customer's tracking link, to find that one order.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

GET/orders/{reference}
One order, with its items and stops
Parameters
referencepath

The order's reference, like TD-4821.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

404

No such order.

PATCH/orders/{reference}
Move an order to a stage
Parameters
referencepath

The order's reference.

Body
stagestringrequired

A stage key from GET /stages.

notestring
Answers
200

OK

400

No stage with that key.

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

404

No such order.

GET/stages
The business's order stages, in order

Each stage has a phase, which is what Orbo acts on: a pickup stage opens a pickup stop, a delivery stage opens a delivery stop.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

BillsGST tax invoices and plain bills, made from an order or at the counter. Batch bills for accounts cover many orders.

POST/bills
Bill an order, or make a bill at the counter
Parameters
Idempotency-Keyheader

8–100 letters, digits, dashes or underscores. A retried request with the same key returns the first answer instead of doing the thing twice. Kept a day.

Body
orderReferencestringrequired
customerobject
notesstring
discountCodestring

One of the business's published discounts (GET /discounts), taken off before tax.

storestringrequired

The store's code.

itemsarrayrequired
customerobject
notesstring
discountCodestring

One of the business's published discounts (GET /discounts), taken off before tax.

Answers
201

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

409

The order already has a bill.

GET/bills
List bills
Parameters
orderquery

An order reference.

statusquery

issued or cancelled.

qquery

Number, order or customer.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

GET/bills/{id}
One bill, with its lines and tax
Parameters
idpath

The bill's id.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

404

No such bill.

PaymentsRazorpay payment links on the business's own account. Needs Billing & payments.

POST/payments
Ask for a payment

With an order and no amount, asks for what the order still owes. An open link for the same ask is reused unless reuse is false.

Parameters
Idempotency-Keyheader

8–100 letters, digits, dashes or underscores. A retried request with the same key returns the first answer instead of doing the thing twice. Kept a day.

Body
orderReferencestring
amountPaiseinteger
descriptionstring
customerobject
notifyboolean

Send the link to the customer.

reuseboolean
Answers
201

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

GET/payments
An order's payments
Parameters
orderReferencequery

The order's reference.

Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

InventoryStock in every store, counts and deliveries in, forecasts of what runs out, and what to order.

GET/inventory
Every item with its stock and forecast
Answers
200

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

POST/inventory/{id}/movements
Record stock coming in, going out, or counted
Parameters
idpath

The item's id.

Body
kindstringrequired

One of received, used, wasted, counted.

amountnumberrequired
costPaiseinteger
notestring
Answers
201

OK

401

No key, or the key is not valid or was revoked.

403

The service this call belongs to is switched off for the business.

OverviewThe business at a glance.

GET/summary
The business at a glance

Orders and their value, where open orders are, deliveries, money in and owed, stock running low, new and returning customers. Sections appear for the services that are on. For an owner's dashboard, not for doing the work.

Parameters
daysquery

1–90, default 7.

Answers
200

OK

401

Not a valid key.