# Showday API

Showday calls your leads with your AI voice agent and records what happened.
This API lets another tool — a lead-generation form, a CRM, an ads platform, an
AI agent — send leads in and read the results back.

Base URL: `https://api.voice.oliotesting.in`
OpenAPI spec: `https://api.voice.oliotesting.in/openapi.json` · This page: `https://api.voice.oliotesting.in/docs`

## 1. Get a key

In Showday: **Settings → API → New key** (owners and admins). The key is shown
once; store it in your tool's secret store. Two kinds:

| Scope | Can do | Give it to |
|---|---|---|
| `leads` | Send leads, read leads back, download the template | Lead-generation tools, forms, CRMs |
| `full` | Everything a tenant admin can do in the app, except the list below | Your own back office, an agent you trust |

No key can: make or revoke keys, change passwords or invite people, change the
Sarvam / Google / WhatsApp connections, change the consent policy, change the
plan, use super admin, or use the in-app copilot. A key calls only leads already
in your account — never a bare phone number. Revoke a key any time in Settings → API; it stops working
within 30 seconds.

Send it on every request:

```
X-API-Key: sdk_live_…
```
(`Authorization: Bearer sdk_live_…` also works.)

Limits: 120 requests a minute for a `leads` key, 300 for `full`; 200 leads per
call. Over the limit you get `429` — wait a minute and send the rest.

## 2. Send leads (the main thing)

`POST https://api.voice.oliotesting.in/intake/leads`

```bash
curl -X POST https://api.voice.oliotesting.in/intake/leads \
  -H "X-API-Key: $SHOWDAY_KEY" -H "Content-Type: application/json" \
  -d '{"leads":[{"phone":"9820012345","name":"Anil Mehta","lead_id":"LG-1001","language":"hi","city":"Mumbai"}]}'
```

Body: one lead object, an array of them, or `{ "leads": [...], "hold": false }`.

| Field | Required | Notes |
|---|---|---|
| `phone` | yes | Indian mobile. `9820012345`, `+91 98200 12345`, `09820012345` all work. Saved as `+919820012345`. |
| `name` | no | The agent greets them by it. |
| `lead_id` | no | Your id. Sending the same `lead_id` again **updates** that lead instead of adding a second one. Empty → the phone number is used. |
| `language` | no | en, hi, mr, ta, te, kn, bn, gu, ml, pa (or the name, e.g. `Hindi`). Empty → the agent's default. |
| `city`, `product`, `tags`, `notes` | no | Free text. `product` is what they enquired about. |
| `consent` | no | `yes` / `no`. Empty counts as yes. `no` → added, never called. |
| `dnd_status` | no | `dnd` → added, never called. |
| `call_after` | no | Don't call before this time, e.g. `2026-10-25 11:00` (IST). |
| `priority` | no | Whole number; higher is called first. |
| your account's own fields | no | e.g. `Budget`, `BHK` for real estate. `GET /intake/columns` lists them. |
| consent evidence | no | `consent_at` (ISO time), `consent_source` (`website_form`, `api`, …), `policy_version`, `consent_text`, `form_url`, `consent_ip`, `user_agent`, `captured_by`, `consent_channel`, `consent_note`. Stored with the lead as the record of how they agreed. |

`hold: true` adds the leads without calling them until someone presses
**Release** in Settings → Journey.

What happens next: each new lead is called by your Sarvam agent automatically,
inside your calling window, under the same rules as leads from your Google
Sheet — consent, DND, do-not-call list, daily dial cap, credit. You do not call
a "dial" endpoint.

Response:

```json
{
  "received": 2, "added": 1, "updated": 0, "rejected": 1, "hold": false,
  "warnings": ["1 phone number will be saved in +91 format."],
  "results": [
    { "index": 0, "status": "added", "leadId": "cm…", "lead_id": "LG-1001", "phone": "+919820012345",
      "calling": { "started": true, "dialAt": "2026-10-22T05:31:00.000Z" }, "errors": [], "warnings": [] },
    { "index": 1, "status": "rejected", "leadId": null, "lead_id": null, "phone": "12345",
      "calling": null, "errors": ["phone \"12345\" is not an Indian mobile number (10 digits starting 6–9)"], "warnings": [] }
  ]
}
```

`status` is `added`, `updated`, `rejected` (fix the `errors` and send it
again) or `failed` (our side; safe to retry). Rejected leads are never added.
`calling.reason` says why a lead was not started: `held`, `auto_journey_off`,
a consent/DND reason, or `already_started`.

## 3. Read results back

Store the `leadId` from the push, then:

- `GET https://api.voice.oliotesting.in/leads/{leadId}` — one lead: `callStatus`, `journeyState`, `disposition`, attempts, transcript summary.
- `GET https://api.voice.oliotesting.in/journey/leads/{leadId}/timeline?limit=200` — everything that happened, oldest first: `{ events: [{ id, type, toState, channel, payload, createdAt }] }`. Event `id` is stable — use it to skip events you have already seen.
- `GET https://api.voice.oliotesting.in/leads?q=9820012345` — search by phone, name or your `lead_id`.
- `GET https://api.voice.oliotesting.in/leads?createdFrom=2026-10-01&journeyState=INTERESTED,CALLBACK&page=1&pageSize=50` — filtered list. Response `{ total, page, pageSize, rows }`.

`callStatus`: PENDING, QUEUED, DIALING, CONNECTED, COMPLETED, NO_ANSWER, BUSY, FAILED, DO_NOT_CALL.
`journeyState`: NEW, DIAL_SCHEDULED, DIALING, NO_CONNECT, WA_NUDGED, CONNECTED, INTERESTED,
CALLBACK, NOT_INTERESTED, VISIT_PENDING_CONFIRM, VISIT_CONFIRMED, VISIT_SHOWN, NO_SHOW, HOT_HANDOFF, DNC.

Poll at most once a minute per lead; most calls finish within a few minutes of the dial.

## 4. Everything else (`full` keys)

| What | Request |
|---|---|
| Call log | `GET /calls?page=1&pageSize=50` · `GET /calls/{attemptId}` · `GET /calls/live` · `GET /calls/export.csv` |
| Call one lead now | `POST /calls/instant` `{ "leadId": "…" }` — checked against consent, DND, do-not-call, credit and the calling window |
| Call leads at a time | `POST /calls/schedule` `{ "leadIds": ["…"], "at": "2026-10-25T11:00:00+05:30" }` |
| Start / release | `POST /journey/leads/{leadId}/start` · `POST /journey/release-held` |
| Do not call | `POST /leads/do-not-call` `{ "leadIds": ["…"], "note": "asked on call" }` |
| Approve / reject / hold | `POST /leads/approve` · `/leads/reject` · `/leads/hold` `{ "leadIds": [...] }` |
| File leads | `POST /leads/bucket` `{ "leadIds": [...], "bucket": "archive" \| "testing" \| null }` |
| Campaigns | `GET /campaigns` · `GET /campaigns/{id}` · `POST /campaigns` `{ "name", "leadIds" }` · `POST /campaigns/{id}/launch` (`{ "launchAt" }` optional) · `/pause` · `/resume` · `/cancel` · `/retry` |
| Appointments | `GET /journey/visits` |
| Numbers | `GET /dashboard/stats` · `GET /analytics/overview` · `GET /analytics/review` |
| What the bot could not answer | `GET /gaps` |
| Balance | `GET /billing/wallet` |
| Calling hours | `GET /integrations/calling-window` |
| Upload a file | `POST /intake/check` then `POST /intake/import` (multipart, field `file`, .csv or .xlsx, up to 2,000 rows; `hold=true` optional) |

## 5. Errors

Errors are JSON: `{ "statusCode": 400, "message": "…" }`. The message is written
for a person — show it.

| Code | Meaning |
|---|---|
| 400 | The request is wrong; the message says what. |
| 401 | Missing, wrong, revoked or expired key. |
| 403 | The key's scope does not cover this route. |
| 404 | Not found in your account. |
| 429 | Over the rate limit. |

## 6. For AI agents

- Read `https://api.voice.oliotesting.in/openapi.json` for exact shapes.
- To get someone called: `POST /intake/leads`. Never loop on `POST /calls/instant` — new leads are called automatically.
- Use your own `lead_id` so retries update rather than duplicate.
- Leads are real people. Send only people who asked to be contacted; send `consent: "no"` or leave them out otherwise.
