AI Prompt Context

Building an integration and want to use AI to write the code faster? Pick the modules you need below and copy the prompt into Copilot, Antigravity, Claude, Cursor, or any other AI assistant as context.

Build your prompt

The Sendiee API surface keeps growing — sending the entire prompt when you only need a couple of modules wastes tokens and dilutes the AI’s focus. Tick only the sections you’re working on and copy the assembled markdown.

Sections to include

18/18 optional sections selected · ~57,114 chars

Always included: Overview · Authentication · Core Concepts · Standard Response Envelope

SENDIEE_QUICK_API_PROMPT.md
# Sendiee API — Quick Reference Prompt File

It covers all Sendiee REST API endpoints, authentication, request/response formats, field definitions, and error codes.

---

---

## Overview

- **Base URL:** `https://api.sendiee.com`
- **API Version:** `v2.0` — all endpoints are prefixed with `/v2.0/`
- **Authentication:** Bearer token in `Authorization` header
- **Content-Type:** `application/json`
- **Official Meta Business Partner** — messaging powered by Meta's WhatsApp Cloud API

---

---

## Authentication

All requests to `/v2.0/*` require:

```
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

API keys are obtained from the Sendiee Dashboard → API & Webhooks.

**Key types & scopes:** Scoped keys (prefix `sk_`, recommended) carry an explicit permission list; legacy keys have full access (deprecation planned). Scopes follow `resource:action` and map to resource paths: `contacts:read|write`, `config:read|write` (fields/custom attributes/tags/lead categories), `messages:send|read`, `chats:read`, `templates:read|write`, `campaigns:read|write`, `assistants:read|write`, `tools:read|write`, `catalogs:read|write`, `channels:read`, `insights:read`, `billing:read`, `integrations:shopify`, `integrations:zoho`, or `*` for full access. POST-based read endpoints (`/contacts/filter`, `/contacts/phone`, `/campaigns/preview`, product search) require the `:read` scope. Calling an endpoint outside a key's scopes returns `403` with code `INSUFFICIENT_SCOPE` and a `required_scope` field naming the missing scope.

---

---

## Core Concepts

### brandNumber
Every WhatsApp messaging endpoint requires a `brandNumber` — the WhatsApp Business phone number registered in your Sendiee account. Format: full international without `+` (e.g. `917012345678` for +91 70123 45678).

### Phone Number Format
All phone numbers use full international format without `+`: `{countryCode}{number}` — e.g. `919876543210`. Pass `defaultCountryCode` to auto-prepend for local numbers. Sendiee sanitises `+`, spaces, dashes, and parentheses automatically.

### Recipient identifier (`to`) — phone OR BSUID
The `to` field on `POST /v2.0/messages/send` and `POST /v2.0/messages/message` is polymorphic. Pass either:

- A **phone number** (E.164 or national + `defaultCountryCode`), or
- A Meta **Business-Scoped User ID (BSUID)** — format `{ISO-3166 alpha-2}.{up to 128 alphanumeric}`, e.g. `IN.abc123def456`.

Sendiee auto-detects the format and forwards it to Meta unchanged. BSUID is used when a WhatsApp user has adopted Meta's Username feature and their phone number is no longer surfaced in webhooks. The `/v2.0/contacts` endpoint also accepts an optional `bsuid` body field for upsert.

> Webhook events (`whatsapp.new_message`, `whatsapp.message_sent`, `whatsapp.message_status`, `contact.created`, etc.) now include an optional `bsuid` field next to `phone`/`from`/`to`. Either may be `null` — treat `bsuid || phone` as the stable recipient identifier going forward.

### Scheduled Messages
Add `scheduledAt` (ISO 8601) and `timezone` (IANA string, default `UTC`) to any send endpoint.  
- Must be a future time — `INVALID_SCHEDULE_TIME` error otherwise.
- Status flow: `scheduled` → `sent` | `failed` | `cancelled`

---

---

## Standard Response Envelope

**Success:**
```json
{
  "success": true,
  "message": "...",
  "data": { ... }
}
```

**Error:**
```json
{
  "success": false,
  "code": "ERROR_CODE",
  "message": "Human-readable description"
}
```

---

---

## WhatsApp API

### 1. Send Template Message

Send an approved WhatsApp message template. Templates can be sent at any time (not restricted by the 24-hour window).

```
POST /v2.0/whatsapp/send
```

**Request Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string | ✅ | Recipient phone (international format or local with `defaultCountryCode`) |
| `brandNumber` | string | ✅ | Sender WhatsApp Business phone number |
| `templateId` | string | ⬜ | Meta template ID (use this or `templateName`) |
| `templateName` | string | ⬜ | Template name from Sendiee Template Manager |
| `language` | string | ⬜ | Template language code. Default: `en` |
| `defaultCountryCode` | string | ⬜ | Country code to prepend to `to` if local |
| `headerParameters` | array | ⬜ | Strings for `{{1}}` in template header |
| `bodyParameters` | array | ⬜ | Strings for `{{1}}`, `{{2}}` etc. in template body |
| `buttonParameters` | array | ⬜ | Strings for dynamic URL buttons |
| `otp` | string/number | ⬜ | Required for AUTHENTICATION category templates |
| `mediaUrl` | string | ⬜ | Required when template header is IMAGE/DOCUMENT/VIDEO |
| `scheduledAt` | string (ISO 8601) | ⬜ | Schedule for future delivery |
| `timezone` | string | ⬜ | IANA timezone. Default: `UTC` |
| `storeInAIContext` | boolean | ⬜ | When `true`, the outgoing message is appended to the recipient's AI conversation history so subsequent AI replies have context of what you sent. Default: `false` |

**Response — Immediate (200 OK):**
```json
{
  "success": true,
  "message": "Message sent successfully",
  "data": {
    "messageId": "wamid.HBgM...",
    "status": "accepted",
    "recipient": "919876543210",
    "template": { "name": "order_confirmation", "id": "1234567890", "language": "en" }
  }
}
```

**Response — Scheduled (201 Created):**
```json
{
  "success": true,
  "message": "Message scheduled successfully",
  "data": {
    "scheduleId": "664abc...",
    "recipient": "919876543210",
    "scheduledAt": "2025-12-01T10:00:00.000Z",
    "timezone": "Asia/Kolkata",
    "template": { "name": "order_confirmation", "id": "1234567890", "language": "en" }
  }
}
```

---

### 2. Send Freeform Message

Send a text, image, document, audio, or video message. Only within the 24-hour customer service window after the recipient last messaged you.

```
POST /v2.0/whatsapp/message
```

**Request Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string | ✅ | Recipient phone |
| `brandNumber` | string | ✅ | Sender WhatsApp Business phone number |
| `type` | string | ✅ | `text` \| `image` \| `document` \| `audio` \| `video` |
| `content` | object | ✅ | Message content — structure depends on `type` |
| `scheduledAt` | string (ISO 8601) | ⬜ | Schedule for future delivery |
| `timezone` | string | ⬜ | IANA timezone. Default: `UTC` |
| `storeInAIContext` | boolean | ⬜ | When `true`, the outgoing message is appended to the recipient's AI conversation history so subsequent AI replies have context of what you sent. Default: `false` |

**Content Object by Type:**

| type | Required fields | Optional fields |
|------|----------------|-----------------|
| `text` | `body` | — |
| `image` | `link` | `caption` |
| `document` | `link`, `filename` | `caption` |
| `audio` | `link` | — |
| `video` | `link` | `caption` |

---

### 3. Message Status

```
GET /v2.0/whatsapp/message/status
```

**Query Parameters:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `messageId` | string | ✅ | The `wamid` returned from the send endpoint |
| `brandNumber` | string | ✅ | Sender WhatsApp Business phone number |
| `retry` | boolean | ⬜ | If `true`, waits 5s and retries once if not found. Default: `false` |

**Status Values:** `accepted` | `sent` | `delivered` | `read` | `failed`

---

---

## Contacts API

### 1. List Contacts

```
GET /v2.0/contacts
```

**Query Parameters:** `page` (default: 1), `limit` (default: 100, max: 100)

---

### 2. Filter Contacts

```
POST /v2.0/contacts/filter
```

**Request Body:** `filter` (object), `page` (default: 1), `limit` (default: 50, max: 500)

**Filterable fields:** `phone`, `countryCode`, `contactName`, `email`, `platform`, `businessName`, `city`, `state`, `postalCode`, `country`, `source`, `lead_category`, `optedOut`, `auto_added`, `customFields.{key}`

Filter rules: string → partial regex; array → `$in` match; number/boolean → exact.

---

### 3. Get Contact by Phone

```
POST /v2.0/contacts/phone
```

**Body:** `phone` (required), `countryCode` (optional)

---

### 4. Create or Update Contact (Upsert)

Returns `isNew: true/false`. If phone exists → update; otherwise → create.

```
POST /v2.0/contacts
```

**Body fields:** `phone` ✅, `countryCode`, `contactName`, `email`, `platform`, `businessName`, `addressLine1`, `addressLine2`, `city`, `state`, `postalCode`, `country`, `source`, `notes`, `lead_category`, `customFields` (key-value object)

---

### 5. List Contact Fields

List all contact fields — both built-in default fields and custom attribute fields. Useful for MCP clients that need to know what fields exist.

```
GET /v2.0/contacts/fields
```

**Query Parameters:**

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `type` | string | `all` | One of: `default`, `custom`, `all` |

**Response (200 OK):**
```json
{
  "success": true,
  "data": [
    { "key": "phone", "label": "Phone", "fieldType": "text", "type": "default" },
    { "_id": "...", "fieldName": "Birthday", "fieldKey": "birthday", "fieldType": "date", "type": "custom" }
  ],
  "meta": { "defaultCount": 19, "customCount": 2, "total": 21 }
}
```

**Default fields (always present):** `phone`, `countryCode`, `contactName`, `email`, `platform`, `businessName`, `addressLine1`, `addressLine2`, `city`, `state`, `postalCode`, `country`, `source`, `notes`, `optedOut`, `lead_category`, `wa_undeliverable`, `createdAt`, `updatedAt`

---

### 6. List Custom Fields

```
GET /v2.0/contacts/custom_fields
```

**Response (200 OK):**
```json
{
  "success": true,
  "data": [
    { "_id": "...", "fieldName": "Birthday", "fieldKey": "birthday", "fieldType": "date", "hasDropdown": false, "options": [] },
    { "_id": "...", "fieldName": "Lead Source", "fieldKey": "lead_source", "fieldType": "dropdown", "hasDropdown": true, "options": ["Facebook Ads", "Google Ads"] }
  ],
  "meta": { "total": 2 }
}
```

---

### 7. Create Custom Field

Subject to `max_custom_attributes` plan limit. `fieldKey` is auto-derived from `fieldName` if omitted, and is **immutable** after creation.

```
POST /v2.0/contacts/custom_fields
```

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `fieldName` | string | ✅ | Display name (e.g. `"Birthday"`) |
| `fieldKey` | string | ⬜ | Storage key — auto-derived if omitted. Immutable once set. |
| `fieldType` | string | ⬜ | `text` \| `number` \| `date` \| `boolean` \| `dropdown`. Default: `text` |
| `options` | string[] | ⬜ | Required (non-empty) when `fieldType` is `dropdown` |

**Response (201 Created):** Returns the created field definition.

---

### 8. Update Custom Field

Only `fieldName`, `fieldType`, and `options` can be updated. `fieldKey` is immutable.

```
PUT /v2.0/contacts/custom_fields/:fieldId
```

**Path:** `fieldId` — MongoDB ObjectId

**Body:** `fieldName`, `fieldType`, `options` (all optional)

**Response (200 OK):** Returns the updated field definition.

---

### 9. Delete Custom Field

Deleting a definition does **not** remove values already stored on contacts.

```
DELETE /v2.0/contacts/custom_fields/:fieldId
```

**Response (200 OK):**
```json
{ "success": true, "message": "Custom field deleted successfully.", "data": { "fieldId": "...", "fieldKey": "lead_source" } }
```

---

### 10. List Contact Tags

Lists all tags alphabetically.

```
GET /v2.0/contacts/tags
```

**Response (200 OK):**
```json
{
  "success": true,
  "data": [
    { "_id": "...", "name": "Hot Lead", "createdAt": "...", "updatedAt": "..." }
  ],
  "meta": { "total": 2 }
}
```

---

### 11. Create Contact Tag

Tag names must be unique. Subject to `max_contact_tags` plan limit.

```
POST /v2.0/contacts/tags
```

**Body:** `name` ✅ — tag name, must be unique within the organisation.

**Response (201 Created):** Returns the created tag object.

---

### 12. Rename Contact Tag

```
PUT /v2.0/contacts/tags/:tagId
```

**Path:** `tagId` — MongoDB ObjectId  
**Body:** `name` ✅ — new name, must be unique.

**Response (200 OK):** Returns the updated tag object.

---

### 13. Delete Contact Tag

```
DELETE /v2.0/contacts/tags/:tagId
```

**Response (200 OK):**
```json
{ "success": true, "message": "Tag deleted successfully.", "data": { "tagId": "...", "name": "Warm Lead" } }
```

---

---

## Scheduled Messages API

### 1. List Scheduled Messages

```
GET /v2.0/scheduled-messages
```

**Query Parameters:** `page` (default: 1), `limit` (default: 50, max: 100), `status` (`scheduled` | `sent` | `failed` | `cancelled`)

---

### 2. Get Scheduled Message

```
GET /v2.0/scheduled-messages/:scheduleId
```

---

### 3. Cancel Scheduled Message

Only cancellable while `status` is `scheduled`.

```
DELETE /v2.0/scheduled-messages/:scheduleId
```

---

---

## Templates API

### 1. List Templates

```
GET /v2.0/templates
```

**Query Parameters:** `status` (`APPROVED` | `PENDING` | `REJECTED`, case-insensitive), `category` (`authentication` | `marketing` | `utility`, case-insensitive)

**Response (200 OK):**
```json
{
  "success": true,
  "data": [ { "_id": "...", "name": "order_shipped_notification", "language": "en", "category": "UTILITY", "status": "APPROVED", "quality_score": "HIGH", "templateId": 302948576120394, "components": [...] } ],
  "total": 1
}
```

### 2. Create Template

Submit a new WhatsApp template for Meta review. Strict validation runs locally first — no preventable rejection ever reaches Meta. Independent rate cap: **2/min, 30/day** per org. Requires `req.waba.wabaReviewStatus === 'APPROVED'`.

```
POST /v2.0/templates
```

**Body — common:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `brandNumber` | string | ✅ | Sender WhatsApp Business phone number |
| `name` | string | ✅ | `^[a-z][a-z0-9_]{0,511}$` |
| `language` | string | ✅ | Locale code, e.g. `en_US` |
| `category` | string | ✅ | `MARKETING` \| `UTILITY` \| `AUTHENTICATION` |
| `header` | object | ⬜ | MARKETING/UTILITY only — see below |
| `body` | object | ⬜ | MARKETING/UTILITY only — `{ text, exampleVariables[] }` |
| `footer` | object | ⬜ | `{ text }` (≤60 chars, no variables) |
| `buttons` | array | ⬜ | ≤10 total; URL ≤2, PHONE_NUMBER ≤1, COPY_CODE ≤1 |
| `auth` | object | ⬜ | AUTHENTICATION only — see below |

**`header` (MARKETING/UTILITY):** `{ format: NONE\|TEXT\|IMAGE\|VIDEO\|DOCUMENT, text?, exampleVariables?[], mediaUrl? (we'll upload), mediaHandle? (pre-uploaded), filename? (DOCUMENT only) }`. For TEXT: max 60 chars, no emoji, max 1 `{{1}}`. For media: provide exactly one of `mediaUrl` (we resumable-upload) or `mediaHandle`.

**`buttons[]`:** `{ type, text?, url?, exampleUrlSuffix?, phone_number?, example? }` — see [Create Template](https://docs.sendiee.com/docs/templates/create) for the full per-type rules.

**`auth` (AUTHENTICATION):** `{ otp_type: COPY_CODE\|ONE_TAP\|ZERO_TAP, add_security_recommendation?, code_expiration_minutes?, supported_apps?[], zero_tap_terms_accepted? }`.

**Response — 201 Created:**
```json
{
  "success": true,
  "message": "Template submitted to Meta for review",
  "data": {
    "templateDbId": "664a...",
    "templateId": 1234567890,
    "name": "diwali_sale_2026",
    "language": "en_US",
    "category": "MARKETING",
    "status": "PENDING"
  }
}
```

**Validation failure — 422:** `{ error: true, code: "TEMPLATE_VALIDATION_FAILED", message, details: [{ path, code, message }] }`.

**Meta rejection — 502:** `{ error: true, code: "META_REJECTED", message, details: { metaErrorCode, metaErrorSubcode, metaMessage, metaUserTitle, metaUserMsg, metaTraceId } }`.

### 3. Get Template

Fetch a single template by its Sendiee `_id` (24-hex ObjectId) **or** its numeric Meta `templateId` — the endpoint resolves whichever you pass. Returns the full template (including `components`) and its resolved `group`.

```
GET /v2.0/templates/{id}
```

**Response (200 OK):**
```json
{ "success": true, "data": { "_id": "664a...", "templateId": 1234567890123456, "name": "order_confirmation", "language": "en_US", "category": "UTILITY", "status": "APPROVED", "components": [], "group": { "_id": "664b...", "name": "Transactional" } } }
```

**Not found — 404:** `{ "error": true, "code": "TEMPLATE_NOT_FOUND", "message": "Template not found" }`. **Invalid id — 400:** `INVALID_TEMPLATE_ID` (id is neither a 24-hex ObjectId nor all-digits).

---

---

## Campaigns API

Send approved templates to many recipients at once — by segment / tag / filter, or by raw contact list. Always **preview first** to confirm the audience size.

### 1. Preview Audience

Returns audience size + sample without persisting anything.

```
POST /v2.0/campaigns/preview
```

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `brandNumber` | string | ✅ | WhatsApp Business sender |
| `audience.segmentIds` | string[] | ⬜ | Include contacts in ANY of these segments |
| `audience.excludeSegmentIds` | string[] | ⬜ | Exclude contacts in these segments |
| `audience.tags` | string[] | ⬜ | Include by tag IDs |
| `audience.excludeTags` | string[] | ⬜ | Exclude by tag IDs |
| `audience.filters` | object | ⬜ | `{ source, hasEmail, optedOut, ignoreUndeliverableContacts, advancedFilters }` |
| `audience.contacts` | object[] | ⬜ | `{ phone, name?, email?, custom? }[]` — **mutually exclusive** with the segment-mode fields |
| `audience.ignoreUndeliverable` | boolean | ⬜ | Default `true` |

**Response (200 OK):**
```json
{
  "success": true,
  "data": {
    "mode": "segment",
    "totalContacts": 1234,
    "breakdown": { "whatsappEligible": 1230, "optedOut": 4, "undeliverable": 0 },
    "sample": [{ "phone": "919876543210", "name": "Surya", "email": null, "optedOut": false }],
    "segmentsUsed": [{ "id": "66a1...", "name": "VIPs" }]
  }
}
```

### 2. Create Campaign

```
POST /v2.0/campaigns
```

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `brandNumber` | string | ✅ | WhatsApp Business sender |
| `campaignName` | string | ✅ | Campaign label |
| `templateId` | string | ⬜ | Sendiee template DB id (preferred) |
| `templateName` | string | ⬜ | Alternative — combine with `language` |
| `language` | string | ⬜ | Used with `templateName` |
| `audience` | object | ✅ | Same shape as the preview body. `contacts[]` entries may also carry `variables` (per-contact values) |
| `variables` | object | ⬜ | See below |
| `schedule` | object | ⬜ | `{ sendAt: ISO, timezone: IANA }`. Omit for immediate |
| `notes` | string | ⬜ | Internal note |
| `dryRun` | boolean | ⬜ | Validate + resolve + return `totalContacts` and a per-contact `resolvedSample` WITHOUT creating/sending or consuming quota (alias `validateOnly`) |

**`variables` shape:**
```json
{
  "header": { "mediaUrl": "https://...", "text": "Surya" },
  "body": ["Surya", "DIWALI25"],
  "buttons": [{ "index": 0, "params": ["DIWALI25"], "payload": "interested_jan" }]
}
```
- `header.mediaUrl` is required when the template header is IMAGE/VIDEO/DOCUMENT.
- `header.text` is required when the template header is TEXT with `{{1}}`.
- `body[i]` length must equal the highest `{{n}}` index in the template body.
- `buttons[i].params` is only required for URL buttons whose URL contains `{{n}}`.

**Per-contact personalization** (both backward-compatible with the static string form):
- **Field reference** — any value (`body[i]`, `header.text`, a `buttons[].params` item) may be a string (static, sent to everyone) **OR** an object `{ field, default? }` resolved per contact. `field` is `name` (contact name), `email`, `city`, `state`, `country`, or `customFields.<key>`. Example: `"body": [{ "field": "name", "default": "there" }]` sends each lead their own name in `{{1}}` — the fix for "same value sent to every lead".
- **Per-contact values** — give each `audience.contacts[]` entry its own `variables` block (raw-contacts mode only). The top-level `variables` still declares structure and supplies fallback defaults.
- **Quick-reply payload** — `buttons[i].payload` (≤256 chars) sets a custom postback returned on the inbound click (in the `campaign.button_click` / `campaign.reply` automation event and to your AI), so two templates with the same button label (e.g. "Interested") can be told apart. Omit to use the default (Meta returns the button label).

**Dry-run example** — `{ ..., "dryRun": true, "variables": { "body": [{ "field": "name" }] } }` → `{ success, dryRun: true, data: { totalContacts, resolvedSample: [{ phone, name, body: ["Asha"] }, ...] } }`.

**Response — 201 Created:**
```json
{
  "success": true,
  "data": {
    "campaignId": "664a...",
    "status": "processing",
    "totalContacts": 432,
    "scheduleType": "immediate",
    "scheduledAt": null,
    "timezone": "UTC"
  }
}
```

**Variable mismatch — 422:** `{ error, code: "CAMPAIGN_VARIABLES_INVALID", message, details: [{ path, code, message }] }`.

**Empty audience — 400:** `{ error, code: "CAMPAIGN_EMPTY_AUDIENCE", message }`.

### 3. List Campaigns

```
GET /v2.0/campaigns?status=completed&brandNumber=...&page=1&limit=20
```

`status` ∈ `draft|scheduled|processing|completed|failed|paused|cancelled`. Returns paginated rows with summary counters per campaign.

### 4. Campaign Report

```
GET /v2.0/campaigns/{campaignId}/report
```

Returns delivery counters, success/read/reply/click rates, cost breakdown by category + country, button-click breakdown, and per-error-code failure breakdown. Cross-tenant requests return `404 CAMPAIGN_NOT_FOUND` (we never reveal whether a foreign id exists).

### 5. Cancel Campaign

```
POST /v2.0/campaigns/{campaignId}/cancel
```

Cancels a campaign that hasn't yet completed. Pending / queued recipients are marked `skipped` so the worker stops draining them. Already-sent messages **cannot be recalled** (Meta has no recall API). Returns `409 CAMPAIGN_NOT_CANCELLABLE` if the campaign is already `completed` / `failed` / `cancelled`. Response: `{ campaignId, status: "cancelled", cancelledAt, skippedRemaining }`.

---

---

## Assistants API

Manage AI assistants (the configuration that drives auto-replies on a channel). One assistant per channel per org — connecting a channel to a new assistant detaches it from the previous one automatically.

Every create / update / toggle / channel-change writes a versioned snapshot to the assistant's edit history with `changedBy.source = "api"` so dashboard audit logs surface API-driven changes alongside human ones.

### 1. Options (supported enum values)

```
GET /v2.0/assistants/options
```

Returns `{ providers, platforms, providerModels:{openai,gemini,groq,anthropic}, voiceProviders, ttsModels, voiceNames, limits:{maxTokens,temperature,topP,memoryMinutes,messageDelay,disableDuration} }`. Fetch this once to learn the supported values; they stay in sync with the dashboard.

### 2. List Assistants

```
GET /v2.0/assistants?platform=whatsapp&name=sales&isActive=true&isConnected=true&page=1&limit=20
```

Filters: `platform` (whatsapp|instagram|messenger), `name` (case-insensitive substring), `isActive`, `isConnected` (true → has phone or pageId).

### 3. Get Assistant

```
GET /v2.0/assistants/{modelId}
```

### 4. Create Assistant

```
POST /v2.0/assistants
```

**Required:** `name`, `provider`, `model`. Provider/model pairing is validated; mismatches return 422 `MODEL_INVALID_FOR_PROVIDER`.

**Channel binding (optional):** pass `channel: { activate: true, platform: "whatsapp"|"instagram"|"messenger" }`. The API resolves the org's existing channel — you don't pass IDs. For multi-WABA orgs add `channel.brandNumber` to disambiguate. If another assistant currently owns the channel, it's detached automatically.

**Configurable fields** (all optional unless noted):
- AI: `systemPrompt` (≤32000), `maxTokens` (1–8000, default 250), `temperature` (0–1, default 0.7), `topP` (0–1, default 1), `tools[]`, `voiceEnabled`, `imageEnabled`, `mediaInToolResponses`, `memoryMinutes` (0–10080, default 60).
- Behavior: `messageDelay` (3–60, default 3), `typingEnabled`, `sendSeen`, `enableCommand`, `disableCommand`, `disableDuration` (0–10080, default 30), `disableOnInterrupt`, `groupChatEnabled`, `groupActivateOnMention`.
- Voice (TTS): `voiceResponseEnabled`, `voiceProvider` (gemini|sarvam|minimax|elevenlabs), `voiceModel`, `voiceName`, `voicePrompt`.
- Audience: `phoneNumbersList[]`, `isNumbersExcluded` (true → deny list, default).

Plan limit: `max_models`. Validation failures return 422 `ASSISTANT_VALIDATION_FAILED` with `details: [{path, code, message}]`.

### 5. Update Assistant

```
PATCH /v2.0/assistants/{modelId}
```

Partial update — only the fields you pass change. To manage the channel binding, send `channel: { activate: true, platform: "..." }` to (re)connect, or `channel: { deactivate: true }` to disconnect.

Optional `clearChatHistory: true` also flushes the AI service's per-channel chat memory.

### 6. Toggle Assistant

```
POST /v2.0/assistants/{modelId}/toggle
{ "isActive": true | false }
```

Quick enable/disable. Writes a `toggle` history entry.

### 7. Delete Assistant

```
DELETE /v2.0/assistants/{modelId}
```

Hard-deletes the assistant and its history.

### 8. Edit History

```
GET /v2.0/assistants/{modelId}/history
```

Last 10 entries. Each: `{ versionNumber, changeType: "create"|"config_update"|"toggle"|"channel_update"|"restore", changeDescription, changedAt, changedBy:{name,email,source}, snapshot }`. API-driven changes have `changedBy.source = "api"`.

### 9. Restore

```
POST /v2.0/assistants/{modelId}/restore
{ "versionNumber": 2, "clearChatHistory": false }
```

Restores the snapshot of a prior `create` or `config_update` version. Channel binding and `isActive` flag are preserved — only config rolls back. The restore is itself written as a new history entry.

---

---

## Tools API

Create and manage the tools an assistant can use. A tool is created once, then attached to assistants by putting its `_id` in the assistant's `tools[]` array (via the Assistants API — there is no separate attach endpoint). A tool attached to no assistant is valid but not callable.

**8 tool types** (`toolType`, immutable after creation): `function` (calls your HTTP endpoint), `static_tool` (fixed reply), `media_attachments` (1–5 library files), `message_templates` (WhatsApp interactive), `instagram_templates`, `messenger_templates`, `sendiee_catalog` (built-in catalog actions), `shopify` (built-in Shopify actions).

**Validation & self-correction:** every create/update is validated server-side (including Meta's WhatsApp/IG/Messenger limits). Failures return `422 TOOL_VALIDATION_FAILED` with `details: [{ path, code, message, example? }]`. The recommended agent flow: call `GET /v2.0/tools/schema?type=<toolType>` first to get the exact shape + a valid `example`, build the payload, POST it, and on `422` fix the listed fields (use each `example`) and retry. Secrets (`auth.token`, `auth.password`, `auth.apiKeyValue`, `secretToken`) are masked in responses.

### 1. Get Schema (call this first)

```
GET /v2.0/tools/schema           # all types
GET /v2.0/tools/schema?type=shopify
```

Returns `{ toolType, title, summary, commonFields, fields, example }` for one type, or `{ toolTypes, commonFields, types:{...} }` for all. Each `example` is a complete, valid create payload.

### 2. List Tools

```
GET /v2.0/tools?toolType=function&isActive=true&tag=<tagId>&name=order&page=1&limit=20
```

### 3. Get Tool

```
GET /v2.0/tools/{toolId}
```

### 4. Create Tool

```
POST /v2.0/tools
```

**Always required:** `name` (lowercase letters/numbers/underscores, starts with a letter, unique per org), `description`, `toolType`. Type-specific fields follow (see schema). Plan limit: `max_tools` → `403 LIMIT_EXCEEDED`. Duplicate name → `409 TOOL_NAME_EXISTS`.

Per-type required fields (beyond the common three):
- `function`: `serverUrl` (http(s), `{{param}}` allowed). Optional `requestMethod` (default POST), `auth` ({type:none|bearer|api_key|basic,…}), `headers[]` ({key,value,enabled}), `bodyMode` (auto|custom|none; custom needs `customPayload` valid JSON), `parameters` (JSON-schema; property names lowercase/underscore), `timeout` (1–300, default 30), `strict`, `async`.
- `static_tool`: `response`. Optional `isStrict`. Supports `{{current_date}}`,`{{current_time}}`,`{{current_datetime}}`,`{{phone_number}}`.
- `media_attachments`: `mediaIds` (1–5 Sendiee File ids). Optional `response` (caption), `isStrict`.
- `message_templates`: `templateType` (button|list|cta_url|product|product_list|catalog_message|flow|carousel) + `components` (exact Meta interactive JSON: `{type, header?, body:{text}, footer?, action}`). Limits: body ≤1024, header ≤60, footer ≤60, ≤3 buttons (unique), list ≤10 sections/≤10 rows (titles ≤24, desc ≤72), cta display ≤20 + valid url, product_list ≤10 sections/≤30 products, carousel 2–10 cards.
- `instagram_templates`: `instagramComponents.message` with text / `quick_replies` (≤13) / `attachment` template (button ≤3 buttons; generic ≤10 elements, ≤3 buttons each). Buttons: web_url(needs url) | postback(needs payload).
- `messenger_templates`: `messengerComponents.message` (same shape/limits as IG; media attachment needs `payload.url`).
- `sendiee_catalog`: `catalogAction:{ type:list_collections|list_products|get_product|get_collection_products, catalogId, collectionId?, defaultLimit? }`. `get_collection_products` requires `collectionId`.
- `shopify`: `shopifyAction:{ type, defaultLimit? }` where type ∈ search_products|get_product|get_variants|list_collections|get_collection_products|track_order|get_order|search_customer|list_customer_orders|create_draft_order|send_invoice. Requires a connected Shopify store at runtime.

### 5. Update Tool

```
PATCH /v2.0/tools/{toolId}
```

Partial — send only changed fields. The patch is merged over the existing tool and the full result re-validated. `toolType` is immutable (`400 TOOL_TYPE_IMMUTABLE`). Subdocuments (`components`, `parameters`, `headers`, `catalogAction`, `shopifyAction`) replace as a whole when included; omit to leave unchanged.

### 6. Toggle Tool

```
POST /v2.0/tools/{toolId}/toggle
{ "isActive": true | false }    # omit body to flip
```

### 7. Delete Tool

```
DELETE /v2.0/tools/{toolId}
```

Deletes the tool, detaches it from all assistants, refreshes their caches, and removes its history.

### 8. History & Restore

```
GET  /v2.0/tools/{toolId}/history
POST /v2.0/tools/{toolId}/restore   { "versionNumber": 2 }
```

Last 10 versions. `changeType`: create|config_update|toggle|restore. API changes have `changedBy.source = "api"`. Restore rolls config back (not `isActive`) and is itself recorded.

### 9. Execution Logs (runs)

```
GET /v2.0/tools/{toolId}/runs?page=1&limit=20
```

Recent invocations (15-day retention). Each: `{ traceId, toolName, toolType, status, errorMessage, durationMs, httpStatus, requestUrl, requestMethod, args, result, createdAt }`. Empty until the tool is attached and called.

Updating/toggling/restoring/deleting a tool automatically clears the AI-service cache for every assistant referencing it, so changes take effect on the next message.

---

---

## Chats API

### 1. Chat History

Paginated message history for one contact (by phone or contactId), newest first.

```
GET /v2.0/chats/history?phone=919876543210&limit=20&before=2026-04-29T05:18:00.000Z
```

| Query | Type | Description |
|-------|------|-------------|
| `phone` | string | Recipient phone (international). One of `phone` / `contactId` required |
| `contactId` | string | Sendiee Contact id |
| `defaultCountryCode` | string | Used with local `phone` |
| `limit` | number | 1–100. Default 20 |
| `before` | ISO string | Cursor — return messages older than this timestamp |

If the phone has no Contact row → `404 CONTACT_NOT_FOUND`. Response carries `pagination.nextCursor` for the next page.

Each message: `{ messageId, wamid, direction: "inbound"|"outbound", status, type, text, media:{url,type}, components, reactions, campaignId, cost:{amount,currency,category}, createdAt }`.

### 2. List Conversations

Inbox view — conversations sorted by `lastMessageAt` desc.

```
GET /v2.0/chats/conversations?archived=false&search=...&page=1&limit=20
```

Each row: `{ conversationId, phone, contactName, brandNumber, lastMessage, lastMessageAt, unreadCount, isArchived, isMuted, isPinned, isStarred, summary, createdAt }`.

---

---

## Insights API

### 1. Daily Summary

Per-day rollups of AI conversations, messages, audio, tokens, cost, and model breakdown. One doc per `(date, platform)`. Best for AI agents generating reports.

```
GET /v2.0/insights/daily-summary?from=2026-04-01&to=2026-04-30&platform=whatsapp
```

| Query | Type | Description |
|-------|------|-------------|
| `from` | YYYY-MM-DD | Default 30 days ago |
| `to` | YYYY-MM-DD | Default today |
| `platform` | string | `whatsapp` \| `instagram` \| `messenger` \| `all` |

Response structure: `{ date, platform, cost:{totalMillidollars}, conversations:{count,uniqueUsers,totalMessages,userMessages,assistantMessages,totalDurationSec}, media:{incoming,outgoing,audioSeconds}, ai:{toolsUsed,inputTokens,outputTokens,cachedTokens,followUpCount,modelBreakdown[]}, hourlyConversations[] }`. Costs are in **millidollars** — divide by 1000 for USD.

### 2. Pipeline Snapshots

Daily lead-pipeline snapshot per org — stage counts, new contacts per platform, opt-outs, never-categorized counts. Powers Lead Studio insights. Snapshots auto-expire after 365 days.

```
GET /v2.0/insights/pipeline-snapshots?platform=whatsapp&from=2026-04-01&to=2026-04-30
```

Response: `{ date, stageCounts:{ "Stage Name": count }, newContacts:{ platform: count }, optOuts, neverCategorized }[]`.

---

---

## Billing API

### 1. Current Plan & Limits

Single source of truth for what an org can do today: plan name, all numeric limits with current usage + remaining headroom, all feature flags.

```
GET /v2.0/billing/plan
```

Response: `{ plan:{name,description,pricing}, subscription:{...}, limits:{ <key>:{ limit, used, remaining } }, features:{ <flag>: bool } }`.

`limits` includes every numeric plan key — `max_contacts`, `max_templates`, `max_campaigns_per_month`, `max_conversations_per_month`, `api_rate_limit_per_sec`, `api_rate_limit_per_day`, etc. `used`/`remaining` are `null` for limits not currently tracked in `UsageTracking` (e.g., rate limits).

### 2. Subscription History

```
GET /v2.0/billing/subscriptions?status=active&page=1&limit=20
```

Each row: `{ subscriptionId, plan:{id,name}, billingCycle, status, paymentStatus, startDate, endDate, amountPaid, currency, createdAt }`.

### 3. Payment History

```
GET /v2.0/billing/payments?status=SUCCESS&paymentFor=subscription&page=1&limit=20
```

`status` ∈ `PENDING|SUCCESS|FAILED`. `paymentFor` ∈ `subscription|wallet_topup|wallet_auto_recharge|subscription_auto_renew`.

Each row: `{ paymentId, orderId, amount, baseAmount, amountUSD, currency, tax:{gst,gstPercent,transactionCharge,transactionChargePercent,totalTax}, status, paymentFor, gateway, subscriptionId, isRecurring, createdAt }`.

---

---

## Leads API

**Lead segmentation** (AI Lead Studio) auto-classifies inbound leads and re-engages them. The whole config is ONE object per org with four parts:

- **Settings** — `isCategorizationEnabled` (master switch), `isFollowupEnabled`, `delay` (seconds before classification runs), `model`, `provider`, `triggerPrompt`, `categorizationToolDescription`.
- **Categories** — buckets the AI sorts each lead into: `{ name, description, fuDisabled, followups[] }`. The chosen bucket is stored on the contact as `lead_category`. Addressed by **name** (case-insensitive). The `description` tells the AI when to use the bucket.
- **Insights** (audience segments) — structured fields the AI extracts from chats: `{ key, name, valueType (text|number|array), options[], description }`. Extracted values land on the contact as `lead_segments`. Addressed by **key** (`^[a-zA-Z][a-zA-Z0-9_]*$`, immutable). The `description` guides what to extract.
- **Follow-ups** — timed re-engagement steps `{ delay (seconds from classification), prompt, active }` attached to a category, falling back to a shared **default** set.

Writes are plan-gated like the dashboard: enabling a feature your plan disables → `403 PLAN_FEATURE_DISABLED`; exceeding `max_lead_categories` / `max_ai_followups` / `max_ai_insights` → `403 PLAN_LIMIT_EXCEEDED`. Renaming a category cascades to existing contacts; deleting one is refused while contacts still use it (pass `reassignTo`). Recommended flow: read with `GET /leads/segmentation` → create/adjust categories & insights → set follow-ups → enable via settings.

### 1. Get the full configuration

```
GET /v2.0/leads/segmentation
```

Returns settings + categories (with their follow-ups) + `defaultFollowups` + `audienceSegments` in one payload.

### 2. Replace / apply a full configuration

```
PUT /v2.0/leads/segmentation
```

Bulk-replace — the fastest way to apply a complete template. Only the top-level keys you include are replaced; `categories` / `defaultFollowups` / `audienceSegments` each replace the whole list. Does **not** cascade category renames (use the category PATCH for that).

**Body:** `{ isCategorizationEnabled?, isFollowupEnabled?, delay?, model?, provider?, triggerPrompt?, categorizationToolDescription?, categories?[], defaultFollowups?[], audienceSegments?[] }`

### 3. Update settings only

```
PATCH /v2.0/leads/segmentation/settings
```

Send any subset of the settings scalars (no categories/followups/insights touched).

### 4. List lead categories

```
GET /v2.0/leads/categories
```

**Response (200 OK):**
```json
{
  "success": true,
  "data": [
    { "name": "Hot Lead", "description": "Ready to buy, asked about pricing.", "fuDisabled": false, "followups": [ { "delay": 600, "prompt": "Nudge them to complete the purchase.", "active": true } ] },
    { "name": "Cold Lead", "description": "Just browsing.", "fuDisabled": false, "followups": [] }
  ],
  "meta": { "total": 2, "categorizationEnabled": true }
}
```

### 5. Get / create / update / delete a category

```
GET    /v2.0/leads/categories/{name}
POST   /v2.0/leads/categories                          body: { name, description, fuDisabled?, followups?[] }
PATCH  /v2.0/leads/categories/{name}                   body: { name? (rename → cascades), description?, fuDisabled? }
DELETE /v2.0/leads/categories/{name}[?reassignTo=Other Category]
```

Duplicate name on create → `409 CATEGORY_EXISTS`. Delete while contacts still use it → `409 CATEGORY_IN_USE`; pass `?reassignTo=` an existing category (or empty value to clear) to move them first.

### 6. Follow-ups (replace per target)

```
GET /v2.0/leads/followups                       -> { default[], categories: [{ name, fuDisabled, followups[] }] }
PUT /v2.0/leads/categories/{name}/followups      body: { followups: [ { delay, prompt, active } ] }
PUT /v2.0/leads/followups/default                body: { followups: [ ... ] }
```

Follow-ups count toward `max_ai_followups`. They fire only when `isFollowupEnabled` is on and the category is not `fuDisabled`.

### 7. Insights (audience segments)

```
GET    /v2.0/leads/insights
GET    /v2.0/leads/insights/{key}
POST   /v2.0/leads/insights         body: { key, name, description, valueType?, options?[] }
PATCH  /v2.0/leads/insights/{key}   body: { name?, valueType?, options?[], description? }   (key is immutable)
DELETE /v2.0/leads/insights/{key}
```

Duplicate key on create → `409 INSIGHT_EXISTS`. Insights count toward `max_ai_insights`.

---

---

## Account API

### 1. Wallet Balance

Fetch the organisation's wallet balance in USD.

```
GET /v2.0/account/wallet/balance
```

**Response (200 OK):**
```json
{ "success": true, "data": { "balance": 24.56, "currency": "USD", "isActive": true } }
```

---

---

## Channels API

### 1. Channel Status

Check real-time connection health of WhatsApp, Instagram, and Messenger channels. Returns both DB state and live Meta API probe results. Designed for MCP / AI debugging — includes raw Meta error payloads.

```
GET /v2.0/channels/status
```

**Query Parameters:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `channel` | string | ⬜ | Filter: `whatsapp` \| `instagram` \| `messenger`. Omit for all. |
| `brandNumber` | string | ⬜ | Narrow to a specific WhatsApp number. |

**`liveStatus` values:**

| Value | Meaning |
|-------|---------|
| `connected` | Channel is healthy |
| `token_expired` | Access token expired or revoked (Meta error 190) |
| `permission_denied` | App lacks permissions — account likely unlinked (Meta error 10/33) |
| `not_configured` | Channel not set up in DB |
| `missing_credentials` | Configured but missing accessToken or account ID |
| `error` | Other Meta API error — inspect error object |
| `unknown` | Could not determine status |

**Response (200 OK) — healthy WhatsApp account:**
```json
{
  "success": true,
  "data": {
    "whatsapp": [{
      "db": { "businessName": "Acme Store", "brandNumber": "919876543210", "isActive": true, "phoneQualityRating": "GREEN", "messagingTier": "TIER_1K" },
      "live": { "liveStatus": "connected", "phoneNumber": { "verified_name": "Acme Store", "quality_rating": "GREEN", "status": "CONNECTED" }, "phoneNumberError": null }
    }]
  },
  "meta": { "accountsChecked": 1, "whatsappCount": 1, "instagramCount": 1, "messengerCount": 1 }
}
```

**Response — token expired:**
```json
{
  "live": {
    "liveStatus": "token_expired",
    "phoneNumber": null,
    "phoneNumberError": { "httpStatus": 401, "metaErrorCode": 190, "metaErrorSubcode": 463, "message": "Error validating access token: Session has expired." }
  }
}
```

---

---

## MCP Server (for AI agents)

Sendiee hosts a Model Context Protocol server at **`https://mcp.sendiee.com`** that exposes the entire v2.0 surface as 73 named tools plus 3 playbook prompts. Any MCP-aware AI client (Claude Desktop, Cursor, Antigravity, claude.ai web) can use it.

- Streamable HTTP: `https://mcp.sendiee.com/mcp` (modern, default)
- Legacy SSE: `https://mcp.sendiee.com/sse` (older clients)
- Health: `https://mcp.sendiee.com/health`

**Auth:** every connection passes the user's Sendiee API key in the standard `Authorization: Bearer <key>` header. The server scopes the key to the session in memory only.

**Claude Desktop config** (uses the `mcp-remote` bridge — Claude Desktop only accepts stdio commands directly):
```json
{
  "mcpServers": {
    "sendiee": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.sendiee.com/mcp",
        "--header",
        "Authorization:Bearer YOUR_SENDIEE_API_KEY"
      ]
    }
  }
}
```

**Cursor / claude.ai web** speak Streamable HTTP natively — point them at the URL with `Authorization: Bearer …` headers.

Full guide: [docs.sendiee.com/docs/mcp](https://docs.sendiee.com/docs/mcp).

---

---

## Webhooks

Webhooks are triggered by the **Automation Builder** in the Sendiee dashboard, not via the REST API. Configure triggers (new message, new contact, AI response, status change) and set a destination HTTP POST URL in Dashboard → Automations. Payload documentation coming soon.

---

---

## Rate Limits

Two rolling limits enforced per organisation:

| Limit | Free | Growth | Pro | Business |
|-------|------|--------|-----|----------|
| Per second (`api_rate_limit_per_sec`) | 5 | 10 | 20 | 60 |
| Per day (`api_rate_limit_per_day`) | 100 | 1,000 | 20,000 | 50,000 |

- Exceeding either limit → `429 Too Many Requests` — check `Retry-After` header
- Error code is `RATE_LIMIT_EXCEEDED_PER_SECOND` or `RATE_LIMIT_EXCEEDED_PER_DAY`
- WhatsApp messaging also subject to **Meta's tier limits**: Tier 1 = 250 unique customers/24h, increases automatically

---

---

## Error Codes Reference

All errors use the format: `{ "success": false, "code": "...", "message": "..." }`

### Rate Limiting (429)

| Code | Description |
|------|-------------|
| `RATE_LIMIT_EXCEEDED_PER_SECOND` | Exceeded per-second request limit |
| `RATE_LIMIT_EXCEEDED_PER_DAY` | Exceeded daily request limit |

### Authentication & Organisation (401 / 403 / 400)

| Code | HTTP | Description |
|------|------|-------------|
| `API_KEY_MISSING` | 401 | No API key in the Authorization header |
| `INVALID_API_KEY` | 401 | API key does not exist or is revoked |
| `AUTHENTICATION_FAILED` | 500 | Internal authentication error |
| `ORG_INACTIVE` | 403 | Organisation account is inactive |
| `DOMAIN_NOT_WHITELISTED` | 403 | Request origin domain not in allowed list |
| `ORG_ID_MISSING` | 400 | Internal: org ID could not be resolved |
| `ORG_ID_REQUIRED` | 400 | Organisation ID could not be resolved from API key |

### Plan Enforcement (403)

| Code | HTTP | Description |
|------|------|-------------|
| `LIMIT_EXCEEDED` | 403 | Numeric plan limit reached (max contacts, tags, etc.). Includes `limitKey`, `current`, `limit` |
| `FEATURE_NOT_ENABLED` | 403 | Feature not available on current plan |

### WhatsApp & Messaging

| Code | HTTP | Description |
|------|------|-------------|
| `BRAND_NUMBER_MISSING` | 400 | `brandNumber` not provided |
| `WABA_NOT_FOUND` | 404 | No active WhatsApp account for given `brandNumber` |
| `WABA_RETRIEVAL_FAILED` | 500 | Internal error retrieving WhatsApp account |
| `MISSING_RECIPIENT` | 400 | `to` field is required |
| `INVALID_PHONE_NUMBER` | 400 | Phone number failed validation |
| `MISSING_TEMPLATE` | 400 | Neither `templateId` nor `templateName` provided |
| `TEMPLATE_NOT_FOUND` | 404 | Template not found or not in APPROVED status |
| `PARAMETER_VALIDATION_ERROR` | 400 | Template parameters count mismatch |
| `INVALID_SCHEDULE_TIME` | 400 | Scheduled time is in the past |
| `META_API_ERROR` | 502 | Error returned by Meta's WhatsApp Cloud API |
| `MESSAGE_SEND_FAILED` | 500 | Internal error sending message |
| `MISSING_CONTENT` | 400 | Message content is empty or missing |
| `MISSING_TEXT_BODY` | 400 | Text message missing `body` field |
| `MISSING_MEDIA_LINK` | 400 | Media message missing `link` field |
| `MESSAGE_NOT_FOUND` | 404 | Message ID not found in this org |
| `STATUS_FETCH_FAILED` | 500 | Internal error fetching message status |
| `SCHEDULE_NOT_CANCELLABLE` | 400 | Message already sent/processing — cannot cancel |

### Contacts

| Code | HTTP | Description |
|------|------|-------------|
| `MISSING_PHONE` | 400 | `phone` field is required |
| `INVALID_PHONE_NUMBER` | 400 | Phone number failed validation or formatting |
| `INVALID_CUSTOM_FIELDS` | 400 | `customFields` is not a plain object |
| `CONTACT_NOT_FOUND` | 404 | No contact found for the given identifier |
| `CONTACT_ALREADY_EXISTS` | 409 | Contact with this phone already exists |

### Contact Fields

| Code | HTTP | Description |
|------|------|-------------|
| `INVALID_FIELD_TYPE_FILTER` | 400 | `type` query param not one of `default`, `custom`, `all` |
| `MISSING_FIELD_NAME` | 400 | `fieldName` is required |
| `INVALID_FIELD_TYPE` | 400 | `fieldType` not one of: `text`, `number`, `date`, `boolean`, `dropdown` |
| `MISSING_DROPDOWN_OPTIONS` | 400 | `options` required and non-empty for `dropdown` type |
| `INVALID_FIELD_KEY` | 400 | Valid `fieldKey` could not be derived |
| `INVALID_FIELD_ID` | 400 | `fieldId` path param not a valid MongoDB ObjectId |
| `CUSTOM_FIELD_NOT_FOUND` | 404 | Custom field not found for this org |
| `CUSTOM_FIELD_ALREADY_EXISTS` | 409 | Custom field with this `fieldKey` already exists |

### Contact Tags

| Code | HTTP | Description |
|------|------|-------------|
| `MISSING_TAG_NAME` | 400 | `name` is required |
| `INVALID_TAG_ID` | 400 | `tagId` path param not a valid MongoDB ObjectId |
| `TAG_NOT_FOUND` | 404 | Tag not found for this org |
| `TAG_ALREADY_EXISTS` | 409 | Tag with this name already exists |

### Wallet

| Code | HTTP | Description |
|------|------|-------------|
| `WALLET_NOT_FOUND` | 404 | No active wallet found for this organisation |

### Channels

| Code | HTTP | Description |
|------|------|-------------|
| `INVALID_CHANNEL` | 400 | `channel` query param not one of `whatsapp`, `instagram`, `messenger` |
| `NO_CHANNELS_FOUND` | 404 | No active business accounts found (or for the specified `brandNumber`) |

### Templates

| Code | HTTP | Description |
|------|------|-------------|
| `TEMPLATE_VALIDATION_FAILED` | 422 | Local pre-Meta validation failed. `details[]` lists each issue |
| `TEMPLATE_RATE_LIMITED` | 429 | Per-org template-create cap (2/min, 30/day) hit |
| `WABA_NOT_REVIEWED` | 409 | Your WABA is not in `APPROVED` review status — Meta will not accept submissions |
| `META_REJECTED` | 502 | Meta returned an error. `details` includes Meta's `code`, `error_subcode`, `message`, `error_user_msg`, `fbtrace_id` |
| `MEDIA_UPLOAD_FAILED` | 502 | We failed to download `header.mediaUrl` or upload it to Meta — `details` includes mime / size / Meta response |
| `TEMPLATE_CREATE_FAILED` | 500 | Internal error during template creation |
| `FAILED_TO_FETCH_TEMPLATES` | 500 | Internal error during list |

### Campaigns

| Code | HTTP | Description |
|------|------|-------------|
| `CAMPAIGN_AUDIENCE_INVALID` | 400 | `audience` block is malformed or refers to an inactive segment |
| `CAMPAIGN_EMPTY_AUDIENCE` | 400 | The resolved audience contains 0 contacts |
| `CAMPAIGN_VARIABLES_INVALID` | 422 | Variables payload doesn't match template requirements (header/body/button) |
| `CAMPAIGN_NAME_REQUIRED` | 400 | `campaignName` missing |
| `CAMPAIGN_TEMPLATE_REQUIRED` | 400 | Neither `templateId` nor `templateName` supplied |
| `TEMPLATE_NOT_FOUND` | 404 | Template not found in your org |
| `CAMPAIGN_TEMPLATE_NOT_APPROVED` | 409 | Template exists but is not `APPROVED` |
| `INVALID_SCHEDULE_TIME` | 400 | `schedule.sendAt` is malformed or in the past |
| `INVALID_CAMPAIGN_STATUS` | 400 | `status` query value is not a valid campaign status |
| `CAMPAIGN_NOT_FOUND` | 404 | Campaign id missing or belongs to another org |
| `CAMPAIGN_NOT_CANCELLABLE` | 409 | Campaign already in terminal status (completed / failed / cancelled) |
| `CAMPAIGN_CANCEL_FAILED` | 500 | Internal error during cancel |
| `CAMPAIGN_PREVIEW_FAILED` | 500 | Internal error during preview |
| `CAMPAIGN_CREATE_FAILED` | 500 | Internal error during create |
| `CAMPAIGN_LIST_FAILED` | 500 | Internal error during list |
| `CAMPAIGN_REPORT_FAILED` | 500 | Internal error during report |

### Assistants

| Code | HTTP | Description |
|------|------|-------------|
| `ASSISTANT_VALIDATION_FAILED` | 422 | Payload failed local validation. `details[]` lists each issue |
| `MODEL_INVALID_FOR_PROVIDER` | 422 | The `model` value is not in the chosen provider's catalog |
| `WABA_NOT_FOUND` | 409 | Channel activation requested but no active WABA exists for the org |
| `INSTAGRAM_NOT_CONNECTED` | 409 | Channel activation requested but the org has no active Instagram |
| `MESSENGER_NOT_CONNECTED` | 409 | Channel activation requested but the org has no active Messenger page |
| `PLATFORM_INVALID` | 400 | Filter or `channel.platform` value is unrecognized |
| `ASSISTANT_NOT_FOUND` | 404 | modelId doesn't exist in your org |
| `IS_ACTIVE_REQUIRED` | 400 | `toggle` body missing the `isActive` boolean |
| `VERSION_REQUIRED` | 400 | `restore` body missing `versionNumber` |
| `HISTORY_VERSION_NOT_FOUND` | 404 | Version doesn't exist for this modelId |
| `VERSION_NOT_RESTORABLE` | 400 | Version is a toggle / channel_update / restore — not rollbackable |

### Chats

| Code | HTTP | Description |
|------|------|-------------|
| `IDENTIFIER_REQUIRED` | 400 | Neither `phone` nor `contactId` was provided |
| `CONTACT_NOT_FOUND` | 404 | Phone / contactId doesn't match a contact in your org |
| `INVALID_BEFORE` | 400 | `before` cursor is not a valid ISO date |
| `CHAT_HISTORY_FAILED` | 500 | Internal error |
| `CONVERSATIONS_LIST_FAILED` | 500 | Internal error |

### Insights

| Code | HTTP | Description |
|------|------|-------------|
| `INVALID_DATE_RANGE` | 400 | `from` / `to` are not YYYY-MM-DD |
| `DAILY_SUMMARY_FAILED` | 500 | Internal error |
| `PIPELINE_SNAPSHOTS_FAILED` | 500 | Internal error |

### Billing

| Code | HTTP | Description |
|------|------|-------------|
| `PLAN_FETCH_FAILED` | 500 | Internal error fetching plan |
| `SUBSCRIPTIONS_FAILED` | 500 | Internal error listing subscriptions |
| `PAYMENTS_FAILED` | 500 | Internal error listing payments |

### Generic

| Code | HTTP | Description |
|------|------|-------------|
| `INTERNAL_SERVER_ERROR` | 500 | Unexpected server-side error |

---

---

## All Endpoints Summary

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/v2.0/whatsapp/send` | Send template message (or schedule one) |
| `POST` | `/v2.0/whatsapp/message` | Send freeform message (or schedule one) |
| `GET` | `/v2.0/whatsapp/message/status` | Check message delivery status |
| `GET` | `/v2.0/contacts` | List all contacts (paginated) |
| `POST` | `/v2.0/contacts/filter` | Filter contacts by fields |
| `POST` | `/v2.0/contacts/phone` | Get a single contact by phone |
| `POST` | `/v2.0/contacts` | Create or update contact (upsert) |
| `GET` | `/v2.0/contacts/fields` | List all contact fields (default + custom) |
| `GET` | `/v2.0/contacts/custom_fields` | List custom field definitions |
| `POST` | `/v2.0/contacts/custom_fields` | Create a custom field |
| `PUT` | `/v2.0/contacts/custom_fields/:fieldId` | Update a custom field |
| `DELETE` | `/v2.0/contacts/custom_fields/:fieldId` | Delete a custom field |
| `GET` | `/v2.0/contacts/tags` | List all contact tags |
| `POST` | `/v2.0/contacts/tags` | Create a contact tag |
| `PUT` | `/v2.0/contacts/tags/:tagId` | Rename a contact tag |
| `DELETE` | `/v2.0/contacts/tags/:tagId` | Delete a contact tag |
| `GET` | `/v2.0/scheduled-messages` | List scheduled messages |
| `GET` | `/v2.0/scheduled-messages/:scheduleId` | Get a scheduled message |
| `DELETE` | `/v2.0/scheduled-messages/:scheduleId` | Cancel a scheduled message |
| `GET` | `/v2.0/templates` | List all WhatsApp templates |
| `GET` | `/v2.0/templates/{id}` | Get one template by `_id` or numeric `templateId` |
| `POST` | `/v2.0/templates` | Submit a new template for Meta review |
| `POST` | `/v2.0/campaigns/preview` | Preview audience size + sample for a campaign |
| `POST` | `/v2.0/campaigns` | Create / schedule a campaign |
| `GET` | `/v2.0/campaigns` | List campaigns (paginated) |
| `GET` | `/v2.0/campaigns/{campaignId}/report` | Full delivery + engagement + cost report |
| `POST` | `/v2.0/campaigns/{campaignId}/cancel` | Cancel a running / scheduled campaign |
| `GET` | `/v2.0/assistants/options` | Supported providers / models / voices |
| `GET` | `/v2.0/assistants` | List assistants (filter by platform, name, isActive, isConnected) |
| `GET` | `/v2.0/assistants/{modelId}` | Get one assistant |
| `POST` | `/v2.0/assistants` | Create assistant + optionally connect channel |
| `PATCH` | `/v2.0/assistants/{modelId}` | Partial update / channel activate / deactivate |
| `POST` | `/v2.0/assistants/{modelId}/toggle` | Quick enable / disable |
| `DELETE` | `/v2.0/assistants/{modelId}` | Delete assistant + its history |
| `GET` | `/v2.0/assistants/{modelId}/history` | Edit history (last 10 versions) |
| `POST` | `/v2.0/assistants/{modelId}/restore` | Restore a prior config version |
| `GET` | `/v2.0/chats/history` | Chat history for a contact (cursor-paginated) |
| `GET` | `/v2.0/chats/conversations` | Inbox-style list of conversations |
| `GET` | `/v2.0/insights/daily-summary` | Per-day AI / message / cost rollups |
| `GET` | `/v2.0/insights/pipeline-snapshots` | Daily lead-pipeline snapshots |
| `GET` | `/v2.0/billing/plan` | Current plan, limits + usage, feature flags |
| `GET` | `/v2.0/billing/subscriptions` | Subscription history |
| `GET` | `/v2.0/billing/payments` | Payment history |
| `GET` | `/v2.0/leads/segmentation` | Get the full lead-segmentation config |
| `PUT` | `/v2.0/leads/segmentation` | Replace / apply a full config (template) |
| `PATCH` | `/v2.0/leads/segmentation/settings` | Update settings only |
| `GET` | `/v2.0/leads/categories` | List AI lead categories |
| `GET` | `/v2.0/leads/categories/{name}` | Get one category |
| `POST` | `/v2.0/leads/categories` | Create a category |
| `PATCH` | `/v2.0/leads/categories/{name}` | Update / rename a category (cascades) |
| `DELETE` | `/v2.0/leads/categories/{name}` | Delete a category (optional `?reassignTo=`) |
| `PUT` | `/v2.0/leads/categories/{name}/followups` | Replace a category's follow-ups |
| `GET` | `/v2.0/leads/followups` | List all follow-ups (default + per category) |
| `PUT` | `/v2.0/leads/followups/default` | Replace the default follow-ups |
| `GET` | `/v2.0/leads/insights` | List AI insights (audience segments) |
| `GET` | `/v2.0/leads/insights/{key}` | Get one insight |
| `POST` | `/v2.0/leads/insights` | Create an insight |
| `PATCH` | `/v2.0/leads/insights/{key}` | Update an insight |
| `DELETE` | `/v2.0/leads/insights/{key}` | Delete an insight |
| `GET` | `/v2.0/account/wallet/balance` | Get wallet balance |
| `GET` | `/v2.0/channels/status` | Check channel connection status |