# API reference (/docs/api) **Base URL** `https://app.orqo.site/api/v1` · **Auth** `Authorization: Bearer ` · **Spec** [`/openapi.json`](/openapi.json) (OpenAPI 3.1) Each page shows the parameters, the request body, every response with its schema, code samples in several languages and a playground. All endpoints share the conventions in [Errors](/docs/errors), [Pagination](/docs/pagination) and [Idempotency](/docs/idempotency). **Try in Playground** sends the request from your browser to `https://app.orqo.site` with the token you paste after `Bearer ` in the Authorization field. It acts on your organization like any other API call (it can create contacts or send messages). Prefer a short-lived token, and revoke it when you're done. # Authentication (/docs/authentication) Every request carries one API token as a bearer token: ```http GET /api/v1/contacts HTTP/1.1 Host: app.orqo.site Authorization: Bearer 1|kQ3x… Accept: application/json ``` ## API tokens [#api-tokens] | | | | --- | --- | | **Where** | **Settings → API Tokens** in the ORQO app. | | **Who** | Organization Admins with two-factor authentication turned on. Each new token asks for a fresh 2FA code. | | **Scope** | One organization: the one that was active when the token was created. Every request acts on it. | | **Lifetime** | Optional expiry date, at most one year ahead. Without one, tokens expire after a year. | | **Shown** | Once, at creation. ORQO stores only a hash. | A token stops working when it is revoked, when it expires, or when its creator stops being an Admin of the organization. A token can read and change every contact, campaign and conversation of the organization and send messages billed to it. Keep it in your server's secret store. Never put it in a browser, a mobile app or a public repository. ## Rotating a token [#rotating-a-token] 1. Create a new token in **Settings → API Tokens**. 2. Deploy your integration with the new token. 3. Revoke the old token from the same page. Revocation is immediate. ## Authentication errors [#authentication-errors] | Status | `error.code` | Meaning | | --- | --- | --- | | 401 | `unauthenticated` | No token, or the token is wrong, revoked or expired. | | 401 | `account_inactive` | The token's user is suspended. | | 402 | `subscription_required` | The organization has no active plan. `GET /me` and `GET /subscription` still work. | | 403 | `forbidden` | The token lacks the `api` ability. Tokens from before the current token format must be reissued. | | 403 | `organization_required` | The token's organization is gone or the user left it. | ```json title="401 Unauthorized" { "error": { "code": "unauthenticated", "message": "Unauthenticated." } } ``` ## Request IDs [#request-ids] Every response carries an `X-Request-Id` header. Send your own (8–128 characters of `A-Z a-z 0-9 . _ -`) to correlate logs, or keep the one ORQO generates and quote it when you contact support. # Errors (/docs/errors) Every failure from `/api/v1` has the same shape, whatever produced it: ```json { "error": { "code": "validation_failed", "message": "The email field must be a valid email address.", "fields": { "email": ["The email field must be a valid email address."] } } } ``` | Field | Always | Meaning | | --- | --- | --- | | `error.code` | yes | Stable, lowercase `snake_case`. **The contract.** | | `error.message` | yes | A sentence for a developer reading a log. Wording changes without notice. | | `error.fields` | no | `validation_failed` only: messages per field, keyed by field name (`template.name` for nested fields). | | `error.details` | no | Extra context for the code, such as `state` or `limit`. | ## Branch on `code`, never on `message` [#branch-on-code-never-on-message] ```js const res = await fetch(url, options); if (!res.ok) { const { error } = await res.json(); switch (error.code) { case 'whatsapp_window_closed': return sendTemplateInstead(); case 'plan_limit_reached': return notifyBilling(error.details); case 'rate_limited': return retryAfter(res.headers.get('Retry-After')); default: // A code this build predates: fall back to the HTTP status. return res.status >= 500 ? scheduleRetry() : failPermanently(error); } } ``` New codes are added as features ship, so always keep a `default` branch. ## Codes [#codes] | Status | Code | When | Retry? | | --- | --- | --- | --- | | 400 | `bad_request` | The request could not be read. | No | | 400 | `invalid_idempotency_key` | `Idempotency-Key` is empty, too long or not printable ASCII. | No | | 401 | `unauthenticated` | Missing, wrong, revoked or expired token. | No | | 402 | `subscription_required` | The organization has no active plan. `details.state` says why. | No | | 402 | `plan_limit_reached` | A plan limit is used up. `details`: `key`, `limit`, `current`. | No | | 402 | `organization_suspended_for_plan`, `channel_suspended_for_plan` | The organization or the channel is over its plan. | No | | 403 | `forbidden` | The token may not do this. | No | | 403 | `organization_required` | The token's organization is no longer available. | No | | 404 | `not_found` | No such resource in your organization, or no such path. | No | | 404 | `contact_not_found`, `whatsapp_channel_not_found` | `contact_id` / `channel_account_id` of a message is not yours. | No | | 405 | `method_not_allowed` | Wrong HTTP method for the path. | No | | 409 | `invalid_state` | The resource's status does not allow the action. `details.status` is the current one. | No | | 409 | `contact_already_exists` | `PATCH /contacts/{id}`: the new phone number belongs to a deleted contact (a live contact with it is a `422`). | No | | 409 | `idempotency_key_reused` | The key was used with a different body. | No | | 409 | `idempotency_in_progress` | The first request with this key is still running. | Yes, shortly | | 422 | `validation_failed` | A field or query parameter is invalid. See `fields`. | No | | 422 | `launch_rejected` | The campaign cannot be launched yet (WhatsApp not ready, no recipients…). | After fixing | | 422 | `whatsapp_window_closed` | The 24-hour service window is closed: send an approved template. | No | | 422 | `whatsapp_template_not_approved` | The template is not approved for this number and language. | No | | 422 | `whatsapp_channel_unavailable` | No active WhatsApp number to send from. | After fixing | | 422 | `contact_phone_required` | The contact has no phone number. | No | | 429 | `rate_limited` | Too many requests. Wait `Retry-After` seconds. | Yes | | 500 | `server_error` | Something broke on our side. | Yes, with backoff | | 502 | `whatsapp_provider_error`, `whatsapp_media_upload_failed` | WhatsApp refused the message. `details` has the provider's code. | Depends on `details` | | 503 | `service_unavailable` | Temporarily unavailable. | Yes, with backoff | The same list is published as JSON at [`/errors.json`](/errors.json). ## Retrying safely [#retrying-safely] - Retry only `409 idempotency_in_progress`, `429`, and `5xx`. Every other `4xx` fails the same way forever. - Back off exponentially (for example 1 s, 2 s, 4 s … capped at 60 s) and honour `Retry-After`. - Send an [`Idempotency-Key`](/docs/idempotency) with every creating request so a retry after a timeout cannot create or send twice. `whatsapp_provider_error` means WhatsApp answered with an error, and the message is stored as failed (`details.message_id`). Retrying with the **same** Idempotency-Key is safe: a definitively failed attempt may be retried, while an accepted one is returned instead of being sent again. # Idempotency (/docs/idempotency) Networks fail after the server has acted. If a `POST` times out you can't know whether the contact was created or the message sent. Send an `Idempotency-Key` and retry with the same key: ORQO acts once and returns the first result to every retry. ```bash curl https://app.orqo.site/api/v1/messages \ -H "Authorization: Bearer $ORQO_API_TOKEN" \ -H "Idempotency-Key: order-1042-shipped" \ -H "Content-Type: application/json" \ -d '{"contact_id": 42, "type": "text", "body": "Your order has shipped"}' ``` ## Which endpoints [#which-endpoints] | Endpoint | Key remembered for | | --- | --- | | `POST /contacts` | 24 hours | | `POST /segments` | 24 hours | | `POST /campaigns` | 24 hours | | `POST /campaigns/{id}/launch` | 24 hours | | `POST /messages` | Permanently, per organization (also accepted as the `client_message_id` body field) | `GET`, `PATCH` and `DELETE` are naturally safe to repeat and ignore the header. ## Rules [#rules] - **The key**: any string of 1–255 printable ASCII characters. Use something tied to the business event (`order-1042-shipped`) or a UUID you store with the job. For `POST /messages` the key is at most 48 characters of `A-Z a-z 0-9 . _ : -`. - **Scope**: per organization (and per user and endpoint for the 24-hour keys). - **A retry** with the same key and the same body returns the stored response with the header `Idempotent-Replayed: true`, and does nothing else. - **The same key with a different body** is `409 idempotency_key_reused`. - **A retry while the first request is still running** is `409 idempotency_in_progress`; wait a moment and retry. - **Server errors are not stored**: a `5xx` response can be retried with the same key and runs again. - **Other errors are stored** (24-hour keys): a `422` or `402` answer is replayed for the same key. After fixing the cause (a field, your plan), retry with a **new** key. # ORQO API (/docs) The ORQO REST API gives your backend the same data your team sees in the ORQO inbox: contacts and segments, WhatsApp campaigns, outbound messages, conversations and analytics. Every request acts on one organization, the one your API token belongs to. ```bash title="Your first request" curl https://app.orqo.site/api/v1/me \ -H "Authorization: Bearer $ORQO_API_TOKEN" ``` ## Rules that hold for every endpoint [#rules-that-hold-for-every-endpoint] - **Base URL** `https://app.orqo.site/api/v1`. JSON in, JSON out. - **Success** is `{"data": ...}`; lists add `"meta"` with a cursor ([Pagination](/docs/pagination)). - **Failure** is `{"error": {"code", "message"}}`. Branch on `code`, never on `message` ([Errors](/docs/errors)). - **Retries** of creating requests are safe with an `Idempotency-Key` header ([Idempotency](/docs/idempotency)). - **Timestamps** are ISO 8601 in UTC (`2026-10-22T09:30:00Z`); IDs are integers. - **Tokens are server-only.** Never ship an API token in a browser or mobile app. ## For AI agents and LLMs [#for-ai-agents-and-llms] These pages are also available as plain Markdown: [`/llms.txt`](/llms.txt) lists every page, [`/llms-full.txt`](/llms-full.txt) holds all of them in one file, the raw spec is at [`/openapi.json`](/openapi.json) and the error catalogue at [`/errors.json`](/errors.json). # Pagination (/docs/pagination) All list endpoints return a page of results and a cursor to the next page: ```json title="GET /api/v1/contacts?limit=2" { "data": [ { "id": 1043, "first_name": "Rahim" }, { "id": 1042, "first_name": "Sara" } ], "meta": { "limit": 2, "has_more": true, "next_cursor": "eyJpZCI6MTA0MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", "prev_cursor": null } } ``` | Parameter | Default | Meaning | | --- | --- | --- | | `limit` | `25` | Items per page, `1` to `100`. | | `cursor` | — | `meta.next_cursor` (forward) or `meta.prev_cursor` (back) from the previous page. | Cursors are opaque: don't build or parse them. A cursor stays valid while the list's filters stay the same, and rows added while you page never make you skip or repeat an item. ## Fetch every page [#fetch-every-page] ```js async function* allContacts() { let cursor = null; do { const url = new URL('https://app.orqo.site/api/v1/contacts'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ORQO_API_TOKEN}` } }); const { data, meta } = await res.json(); yield* data; cursor = meta.has_more ? meta.next_cursor : null; } while (cursor); } ``` ```python def all_contacts(session): params = {"limit": 100} while True: page = session.get("https://app.orqo.site/api/v1/contacts", params=params).json() yield from page["data"] if not page["meta"]["has_more"]: break params["cursor"] = page["meta"]["next_cursor"] ``` ## Order of each list [#order-of-each-list] | Endpoint | Order | | --- | --- | | `GET /contacts`, `/segments`, `/campaigns`, `/campaigns/{id}/recipients`, `/segments/{id}/contacts` | Newest first | | `GET /conversations` | Most recent message first | | `GET /conversations/{id}/messages` | Oldest first (reading order) | | `GET /notifications` | Newest first | Filters are plain query parameters (`?status=open&channel=whatsapp`). An unknown value is a `422 validation_failed`, never silently ignored. # Quickstart (/docs/quickstart) This guide takes about five minutes. You need an ORQO organization with a connected WhatsApp number and an Admin role in it. ### Create an API token [#create-an-api-token] In ORQO open **Settings → API Tokens** (`/app/api-tokens`) and choose **Create token**. Creating a token needs two-factor authentication on your account, and the token is shown **once**: copy it straight into your server's secret store. ```bash export ORQO_API_TOKEN="1|kQ3x…" ``` A token can do everything an Admin can do through the API. Keep it on your server; never put it in a web page or a mobile app. ### Check the token [#check-the-token] ```bash curl https://app.orqo.site/api/v1/me \ -H "Authorization: Bearer $ORQO_API_TOKEN" ``` ```json title="200 OK" { "data": { "id": 7, "name": "Mona Adel", "email": "mona@example.com", "role": "client", "status": "active", "email_verified_at": "2026-09-01T10:00:00Z", "created_at": "2026-09-01T09:58:12Z" } } ``` A `401` with `"code": "unauthenticated"` means the token is wrong, revoked or expired. ### Add a contact [#add-a-contact] ```bash curl https://app.orqo.site/api/v1/contacts \ -H "Authorization: Bearer $ORQO_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: contact-rahim-0001" \ -d '{"phone_e164": "+201001234567", "first_name": "Rahim", "opt_in_whatsapp": true}' ``` ```js const res = await fetch('https://app.orqo.site/api/v1/contacts', { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORQO_API_TOKEN}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'contact-rahim-0001', }, body: JSON.stringify({ phone_e164: '+201001234567', first_name: 'Rahim', opt_in_whatsapp: true }), }); const { data: contact } = await res.json(); ``` ```python import os, requests res = requests.post( "https://app.orqo.site/api/v1/contacts", headers={ "Authorization": f"Bearer {os.environ['ORQO_API_TOKEN']}", "Idempotency-Key": "contact-rahim-0001", }, json={"phone_e164": "+201001234567", "first_name": "Rahim", "opt_in_whatsapp": True}, ) contact = res.json()["data"] ``` ```php $contact = Http::withToken(env('ORQO_API_TOKEN')) ->withHeaders(['Idempotency-Key' => 'contact-rahim-0001']) ->post('https://app.orqo.site/api/v1/contacts', [ 'phone_e164' => '+201001234567', 'first_name' => 'Rahim', 'opt_in_whatsapp' => true, ]) ->throw() ->json('data'); ``` The response is `201 Created` with the contact in `data` (or `200` if a contact with that number already existed: it is updated instead). Keep its `id`. ### Send a WhatsApp message [#send-a-whatsapp-message] A free-form text works while the customer's 24-hour service window is open. Outside it, send an approved template ([Send a message](/docs/api/messages/send-a-message)). ```bash curl https://app.orqo.site/api/v1/messages \ -H "Authorization: Bearer $ORQO_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-shipped" \ -d '{"contact_id": 42, "type": "text", "body": "Your order has shipped 🚚"}' ``` ```json title="201 Created" { "data": { "id": 901, "provider_message_id": "wamid.HBgM…", "status": "sent", "type": "text", "contact_id": 42, "conversation_id": 18, "conversation_uuid": "9b1f…", "channel_account_id": 3, "to": "+201001234567", "sent_at": "2026-10-22T09:30:00Z" } } ``` The message appears in the ORQO inbox in the contact's conversation, and replies come back there. ## What's next? [#whats-next] # Rate limits (/docs/rate-limits) Each token's user may make **60 requests per minute**. Every response says how much is left: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per minute. | | `X-RateLimit-Remaining` | Requests left in the current minute. | | `Retry-After` | On `429` only: seconds until you may retry. | | `X-RateLimit-Reset` | On `429` only: Unix time when the window resets. | ```json title="429 Too Many Requests" { "error": { "code": "rate_limited", "message": "Too Many Attempts." } } ``` When you get a `429`, wait `Retry-After` seconds before the next request. For bulk work (importing thousands of contacts, messaging a list) prefer a [campaign](/docs/api) or spread requests out instead of bursting. Plan limits (contacts, campaigns per month, WhatsApp messages per month) are separate from rate limits and answer `402 plan_limit_reached`; see [Errors](/docs/errors). # Versioning and changelog (/docs/versioning) The version is part of the path: `/api/v1`. Within v1 we only make **additive** changes, which your integration must tolerate: - new endpoints, new optional request fields and new query parameters; - new fields in responses (ignore fields you don't know); - new error `code` values (keep a default branch); - new enum values, such as a new message `status`. Anything else (removing or renaming a field, changing a type or a status code) ships as a new version, announced here at least 90 days ahead. ## Changelog [#changelog] ### 2026-10 — v1 cleanup [#2026-10--v1-cleanup] The API moved to the conventions on these pages. If you integrated before: | Before | Now | | --- | --- | | Errors as `{"error": "…"}`, `{"message": "…", "code": "…"}` or `{"message", "errors"}` | Always `{"error": {"code", "message", "fields"?, "details"?}}` | | `POST /messages/send` → `200` with `idempotent_replay` | `POST /messages` → `201`, result in `data`, replays flagged by the `Idempotent-Replayed` header | | `GET /usage` | `GET /subscription/usage` | | `GET /analytics/campaign/{id}/funnel` | `GET /analytics/campaigns/{id}/funnel` | | Page-number pagination with `links` on campaigns, segments, recipients, notifications and conversation messages | Cursor pagination with `meta` on every list, `?limit=` up to 100 | | `DELETE /contacts/{id}` → `200 {"ok": true}` | `204 No Content` | | `POST /campaigns/{id}/launch` → `200 {"ok", "status"}` | `202` with the campaign in `data` | | `POST /campaigns/{id}/pause`, `PATCH /me` → `{"ok": true}` | The updated resource in `data` | | Editing or pausing a campaign in the wrong status → `422` | `409 invalid_state` | | Campaign fields `template_ref`, `payload_json` | `template`, `payload` | | `organization_id` on every resource | Removed (a token belongs to one organization) | | `GET /subscription` → `{"data": null, "subscription_required": true}` | `{"data": null}` | | Unvalidated filters were ignored | Unknown filter values are `422` | | `POST /contacts` always inserted (a taken phone number was a `500`) | Creates **or updates** the contact matched by `phone_e164` (else `email`), like the app: `201` new, `200` updated; a deleted contact with that number is restored | | `custom_fields` accepted any key | Keys must be contact fields defined in Settings; values are cast to the field's type (`422` otherwise) | | Dynamic segments created through the API stayed empty | They are filled from `rules` on creation | | — | New: `GET /segments/{id}`, `GET /conversations/{id}`, `Idempotency-Key` on creating requests, `X-Request-Id` | # Webhooks (/docs/webhooks) An ORQO automation with the **Webhook Received** trigger gets its own URL. POST JSON to it from your backend (a new order, a failed payment, a booking) and the automation runs: send a WhatsApp template, tag the contact, assign a conversation, and so on. ORQO does not yet send outgoing event webhooks (for example "message delivered" or "conversation resolved") to your servers. Poll the [API](/docs/api) for those. Outgoing webhooks are planned. ## 1. Create the trigger [#1-create-the-trigger] In ORQO open **Automations**, create an automation, pick the **Webhook Received** trigger and choose **Generate token**. Copy two values from the trigger panel: - the **Webhook URL**, `https://app.orqo.site/webhooks/automation/{trigger_token}` - the **Signing secret**. It is shown right after generating; keep it on your server, like an API token. **Regenerate token** issues a new URL and secret; the old ones stop working. Activate the automation: requests to an inactive automation are refused. ## 2. Sign and send the request [#2-sign-and-send-the-request] Every request must carry two headers: | Header | Value | | --- | --- | | `X-Orqo-Timestamp` | Current Unix time in seconds. Requests more than 5 minutes off are refused. | | `X-Orqo-Signature` | `sha256=` + hex HMAC-SHA256 of `{timestamp}.{raw body}` with the signing secret. | Sign the **exact bytes** you send: serialize the JSON once and send that string. ```js import crypto from 'node:crypto'; const body = JSON.stringify({ event: 'order.paid', phone: '+201001234567', order_id: 'A-1042', total: '450.00' }); const timestamp = Math.floor(Date.now() / 1000).toString(); const signature = crypto.createHmac('sha256', process.env.ORQO_TRIGGER_SECRET) .update(`${timestamp}.${body}`) .digest('hex'); await fetch(process.env.ORQO_TRIGGER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Orqo-Timestamp': timestamp, 'X-Orqo-Signature': `sha256=${signature}`, 'Idempotency-Key': 'order-A-1042-paid', }, body, }); ``` ```python import hashlib, hmac, json, os, time, requests body = json.dumps({"event": "order.paid", "phone": "+201001234567", "order_id": "A-1042"}) timestamp = str(int(time.time())) signature = hmac.new(os.environ["ORQO_TRIGGER_SECRET"].encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest() requests.post( os.environ["ORQO_TRIGGER_URL"], data=body, headers={ "Content-Type": "application/json", "X-Orqo-Timestamp": timestamp, "X-Orqo-Signature": f"sha256={signature}", "Idempotency-Key": "order-A-1042-paid", }, ) ``` ```php $body = json_encode(['event' => 'order.paid', 'phone' => '+201001234567', 'order_id' => 'A-1042']); $timestamp = (string) time(); $signature = hash_hmac('sha256', "{$timestamp}.{$body}", env('ORQO_TRIGGER_SECRET')); Http::withBody($body, 'application/json') ->withHeaders([ 'X-Orqo-Timestamp' => $timestamp, 'X-Orqo-Signature' => "sha256={$signature}", 'Idempotency-Key' => 'order-A-1042-paid', ]) ->post(env('ORQO_TRIGGER_URL')); ``` ```bash BODY='{"event":"order.paid","phone":"+201001234567","order_id":"A-1042"}' TS=$(date +%s) SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$ORQO_TRIGGER_SECRET" -hex | sed 's/^.* //') curl "$ORQO_TRIGGER_URL" \ -H "Content-Type: application/json" \ -H "X-Orqo-Timestamp: $TS" \ -H "X-Orqo-Signature: sha256=$SIG" \ -d "$BODY" ``` ## 3. The payload [#3-the-payload] Any JSON object. Its fields are available to the automation's steps as variables. Two fields are special: | Field | Effect | | --- | --- | | `email` | The automation runs for the contact with this email, if one exists. | | `phone` | Otherwise, the contact with this E.164 phone number. | ## Responses [#responses] | Status | Body | Meaning | | --- | --- | --- | | `202` | `{"status": "accepted"}` | The automation was started. | | `200` | `{"status": "duplicate"}` | Same signature or same `Idempotency-Key` seen before: nothing ran again. | | `404` | `{"error": "Automation not found or inactive."}` | Wrong URL, inactive automation, or a bad or expired signature. The reason is not revealed on purpose. | Retry on network errors and `5xx` with the same body and `Idempotency-Key`, re-signing with a fresh timestamp. Don't retry a `404`: check the URL, the secret and your clock. # Retrieve the current user (/docs/api/account/retrieve-the-current-user) The user the API token belongs to. ## GET /api/v1/me ```json { "operationId": "retrieve-the-current-user", "description": "The user the API token belongs to.", "summary": "Retrieve the current user", "tags": [ "Account" ], "responses": { "200": { "description": "`User`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/User" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Update the current user (/docs/api/account/update-the-current-user) ## PATCH /api/v1/me ```json { "operationId": "update-the-current-user", "summary": "Update the current user", "tags": [ "Account" ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 } } } } } }, "responses": { "200": { "description": "`User`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/User" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve the organization (/docs/api/account/retrieve-the-organization) The organization the API token is bound to, with your role in it. ## GET /api/v1/organization ```json { "operationId": "retrieve-the-organization", "description": "The organization the API token is bound to, with your role in it.", "summary": "Retrieve the organization", "tags": [ "Account" ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "base_currency": { "type": "string" }, "role": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "name", "base_currency", "role", "created_at" ] } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve the subscription (/docs/api/account/retrieve-the-subscription) The organization's current plan, or `"data": null` when it has no trial or subscription. Readable even without a subscription. ## GET /api/v1/subscription ```json { "operationId": "retrieve-the-subscription", "description": "The organization's current plan, or `\"data\": null` when it has no\ntrial or subscription. Readable even without a subscription.", "summary": "Retrieve the subscription", "tags": [ "Account" ], "responses": { "200": { "description": "No trial or subscription: the organization must subscribe.", "content": { "application/json": { "schema": { "anyOf": [ { "type": "object", "properties": { "data": { "type": "object", "properties": { "plan": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "slug": { "type": "string" } }, "required": [ "id", "name", "slug" ] }, "billing_cycle": { "type": "string" }, "status": { "anyOf": [ { "type": "string" }, { "type": "string", "enum": [ "active", "inactive" ] } ] }, "renews_at": { "type": "string" }, "ends_at": { "type": "string" }, "gateway": { "type": "string" }, "managed_by_admin": { "type": "string" } }, "required": [ "plan", "billing_cycle", "status", "renews_at", "ends_at", "gateway", "managed_by_admin" ] } }, "required": [ "data" ] }, { "type": "object", "properties": { "data": { "type": "null" } }, "required": [ "data" ] } ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve plan usage limits (/docs/api/account/retrieve-plan-usage-limits) ## GET /api/v1/subscription/usage ```json { "operationId": "retrieve-plan-usage-limits", "summary": "Retrieve plan usage limits", "tags": [ "Account" ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "plan_name": { "type": "string" }, "limits": { "type": [ "object", "null" ], "properties": { "users": { "type": "string" }, "storage_mb": { "type": "string" }, "storage_gb": { "type": [ "number", "null" ] } }, "required": [ "users", "storage_mb", "storage_gb" ] } }, "required": [ "plan_name", "limits" ] } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List notifications (/docs/api/account/list-notifications) The current user's in-app notifications, newest first. ## GET /api/v1/notifications ```json { "operationId": "list-notifications", "description": "The current user's in-app notifications, newest first.", "summary": "List notifications", "tags": [ "Account" ], "parameters": [ { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Notification`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Notification" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Mark a notification as read (/docs/api/account/mark-a-notification-as-read) ## POST /api/v1/notifications/{notification}/read ```json { "operationId": "mark-a-notification-as-read", "summary": "Mark a notification as read", "tags": [ "Account" ], "parameters": [ { "name": "notification", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "`Notification`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Notification" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List contacts (/docs/api/contacts/list-contacts) Newest first. Filter by `search` (name, email or phone), `segment_id`, `tag` and `opt_in_whatsapp`. ## GET /api/v1/contacts ```json { "operationId": "list-contacts", "description": "Newest first. Filter by `search` (name, email or phone), `segment_id`,\n`tag` and `opt_in_whatsapp`.", "summary": "List contacts", "tags": [ "Contacts" ], "parameters": [ { "name": "search", "in": "query", "description": "Matches first name, last name, email or phone.", "schema": { "type": "string", "maxLength": 255 } }, { "name": "segment_id", "in": "query", "schema": { "type": "integer", "minimum": 1 } }, { "name": "tag", "in": "query", "description": "Exact tag name.", "schema": { "type": "string", "maxLength": 100 } }, { "name": "opt_in_whatsapp", "in": "query", "schema": { "type": "boolean" } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Contact`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Contact" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Create or update a contact (/docs/api/contacts/create-or-update-a-contact) Contacts are matched like everywhere else in ORQO: by `phone_e164`, or by `email` when no phone is given. A new contact is `201`; an existing one (or a deleted one with that number, which is restored) is updated and returned with `200`. `custom_fields` keys must be contact fields defined in Settings. Send an `Idempotency-Key` header to retry safely. ## POST /api/v1/contacts ```json { "operationId": "create-or-update-a-contact", "description": "Contacts are matched like everywhere else in ORQO: by `phone_e164`, or\nby `email` when no phone is given. A new contact is `201`; an existing\none (or a deleted one with that number, which is restored) is updated\nand returned with `200`. `custom_fields` keys must be contact fields\ndefined in Settings. Send an `Idempotency-Key` header to retry safely.", "summary": "Create or update a contact", "tags": [ "Contacts" ], "parameters": [ { "name": "Idempotency-Key", "in": "header", "description": "Any unique string up to 255 characters, such as a UUID. Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of acting twice.", "schema": { "type": "string" }, "example": "9f1c2a7e-4b8d-4e57-a3f0-6c1d2e3f4a5b" } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "phone_e164": { "type": [ "string", "null" ], "description": "E.164 phone number, e.g. `+201001234567`.", "pattern": "^\\+[1-9]\\d{6,14}$" }, "email": { "type": [ "string", "null" ], "format": "email", "maxLength": 255 }, "first_name": { "type": [ "string", "null" ], "maxLength": 100 }, "last_name": { "type": [ "string", "null" ], "maxLength": 100 }, "country": { "type": [ "string", "null" ], "description": "ISO 3166-1 alpha-2 country code.", "minLength": 2, "maxLength": 2 }, "language": { "type": [ "string", "null" ], "maxLength": 10 }, "opt_in_whatsapp": { "type": [ "boolean", "null" ] }, "custom_fields": { "type": [ "array", "null" ], "description": "Values keyed by contact field key (Settings → Contact fields).", "items": { "type": "string" }, "maxItems": 50 }, "source": { "type": [ "string", "null" ], "description": "Defaults to `api`.", "maxLength": 100 } } } } } }, "responses": { "200": { "description": "An existing contact with this phone or email was updated.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Contact" } }, "required": [ "data" ] } } } }, "201": { "description": "A new contact was created.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Contact" } }, "required": [ "data" ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve a contact (/docs/api/contacts/retrieve-a-contact) ## GET /api/v1/contacts/{id} ```json { "operationId": "retrieve-a-contact", "summary": "Retrieve a contact", "tags": [ "Contacts" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`Contact`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Contact" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Update a contact (/docs/api/contacts/update-a-contact) Only the fields you send change. A `phone_e164` another contact already has is `422 validation_failed`. ## PATCH /api/v1/contacts/{id} ```json { "operationId": "update-a-contact", "description": "Only the fields you send change. A `phone_e164` another contact already\nhas is `422 validation_failed`.", "summary": "Update a contact", "tags": [ "Contacts" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "phone_e164": { "type": "string", "pattern": "^\\+[1-9]\\d{6,14}$" }, "email": { "type": "string", "format": "email", "maxLength": 255 }, "first_name": { "type": "string", "maxLength": 100 }, "last_name": { "type": "string", "maxLength": 100 }, "country": { "type": "string", "minLength": 2, "maxLength": 2 }, "language": { "type": "string", "maxLength": 10 }, "opt_in_whatsapp": { "type": "boolean" }, "custom_fields": { "type": [ "array", "null" ], "items": { "type": "string" }, "maxItems": 50 }, "source": { "type": "string", "maxLength": 100 } } } } } }, "responses": { "200": { "description": "`Contact`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Contact" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Delete a contact (/docs/api/contacts/delete-a-contact) ## DELETE /api/v1/contacts/{id} ```json { "operationId": "delete-a-contact", "summary": "Delete a contact", "tags": [ "Contacts" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List segments (/docs/api/segments/list-segments) Newest first. ## GET /api/v1/segments ```json { "operationId": "list-segments", "description": "Newest first.", "summary": "List segments", "tags": [ "Segments" ], "parameters": [ { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Segment`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Create a segment (/docs/api/segments/create-a-segment) A dynamic segment is filled from its `rules` right away. Send an `Idempotency-Key` header to retry safely. ## POST /api/v1/segments ```json { "operationId": "create-a-segment", "description": "A dynamic segment is filled from its `rules` right away. Send an `Idempotency-Key` header to retry safely.", "summary": "Create a segment", "tags": [ "Segments" ], "parameters": [ { "name": "Idempotency-Key", "in": "header", "description": "Any unique string up to 255 characters, such as a UUID. Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of acting twice.", "schema": { "type": "string" }, "example": "9f1c2a7e-4b8d-4e57-a3f0-6c1d2e3f4a5b" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 200 }, "type": { "type": [ "string", "null" ], "description": "Defaults to `static`.", "enum": [ "static", "dynamic" ] }, "rules": { "type": [ "array", "null" ], "description": "Filter rules of a dynamic segment.", "items": { "type": "string" } } }, "required": [ "name" ] } } } }, "responses": { "201": { "description": "`Segment`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Segment" } }, "required": [ "data" ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve a segment (/docs/api/segments/retrieve-a-segment) ## GET /api/v1/segments/{id} ```json { "operationId": "retrieve-a-segment", "summary": "Retrieve a segment", "tags": [ "Segments" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`Segment`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Segment" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List segment contacts (/docs/api/segments/list-segment-contacts) Newest first. ## GET /api/v1/segments/{id}/contacts ```json { "operationId": "list-segment-contacts", "description": "Newest first.", "summary": "List segment contacts", "tags": [ "Segments" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Contact`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Contact" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List campaigns (/docs/api/campaigns/list-campaigns) Newest first. Filter by `channel` and `status`. ## GET /api/v1/campaigns ```json { "operationId": "list-campaigns", "description": "Newest first. Filter by `channel` and `status`.", "summary": "List campaigns", "tags": [ "Campaigns" ], "parameters": [ { "name": "channel", "in": "query", "schema": { "type": "string", "enum": [ "whatsapp" ] } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "" ] } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Campaign" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Create a campaign (/docs/api/campaigns/create-a-campaign) Creates a draft. Launch it with `POST /campaigns/{id}/launch`. Send an `Idempotency-Key` header to retry safely. ## POST /api/v1/campaigns ```json { "operationId": "create-a-campaign", "description": "Creates a draft. Launch it with `POST /campaigns/{id}/launch`. Send an\n`Idempotency-Key` header to retry safely.", "summary": "Create a campaign", "tags": [ "Campaigns" ], "parameters": [ { "name": "Idempotency-Key", "in": "header", "description": "Any unique string up to 255 characters, such as a UUID. Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of acting twice.", "schema": { "type": "string" }, "example": "9f1c2a7e-4b8d-4e57-a3f0-6c1d2e3f4a5b" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 200 }, "channel": { "type": "string", "enum": [ "whatsapp" ] }, "audience_type": { "type": [ "string", "null" ], "description": "Defaults to `segment`.", "enum": [ "segment", "contact_list", "tag", "csv" ] }, "audience_ref": { "type": [ "string", "null" ], "description": "Segment id, tag name or CSV upload reference, depending on `audience_type`." }, "template": { "type": [ "array", "null" ], "description": "Approved WhatsApp template: name, language and component parameters.", "items": { "type": "string" } }, "payload": { "type": [ "array", "null" ], "description": "Extra message payload, such as header media.", "items": { "type": "string" } }, "schedule_at": { "type": [ "string", "null" ], "format": "date-time", "description": "ISO 8601 time to send at; omit to send on launch." }, "timezone": { "type": [ "string", "null" ], "description": "IANA time zone, e.g. `Africa/Cairo`.", "maxLength": 64 } }, "required": [ "name", "channel" ] } } } }, "responses": { "201": { "description": "`Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } }, "required": [ "data" ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve a campaign (/docs/api/campaigns/retrieve-a-campaign) `stats` are recomputed from the recipients on every call. ## GET /api/v1/campaigns/{id} ```json { "operationId": "retrieve-a-campaign", "description": "`stats` are recomputed from the recipients on every call.", "summary": "Retrieve a campaign", "tags": [ "Campaigns" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Update a campaign (/docs/api/campaigns/update-a-campaign) Only draft and paused campaigns can be edited; any other status is `409 invalid_state`. ## PATCH /api/v1/campaigns/{id} ```json { "operationId": "update-a-campaign", "description": "Only draft and paused campaigns can be edited; any other status is\n`409 invalid_state`.", "summary": "Update a campaign", "tags": [ "Campaigns" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 200 }, "channel": { "type": "string", "enum": [ "whatsapp" ] }, "audience_type": { "type": "string", "enum": [ "segment", "contact_list", "tag", "csv" ] }, "audience_ref": { "type": [ "string", "null" ] }, "template": { "type": [ "array", "null" ], "items": { "type": "string" } }, "payload": { "type": [ "array", "null" ], "items": { "type": "string" } }, "schedule_at": { "type": [ "string", "null" ], "format": "date-time" }, "timezone": { "type": [ "string", "null" ], "maxLength": 64 } } } } } }, "responses": { "200": { "description": "`Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Launch a campaign (/docs/api/campaigns/launch-a-campaign) Queues the campaign for sending (`202`), now or at `schedule_at`. A campaign that cannot be launched is `422 launch_rejected`. Send an `Idempotency-Key` header to retry safely. ## POST /api/v1/campaigns/{id}/launch ```json { "operationId": "launch-a-campaign", "description": "Queues the campaign for sending (`202`), now or at `schedule_at`. A\ncampaign that cannot be launched is `422 launch_rejected`. Send an\n`Idempotency-Key` header to retry safely.", "summary": "Launch a campaign", "tags": [ "Campaigns" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }, { "name": "Idempotency-Key", "in": "header", "description": "Any unique string up to 255 characters, such as a UUID. Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of acting twice.", "schema": { "type": "string" }, "example": "9f1c2a7e-4b8d-4e57-a3f0-6c1d2e3f4a5b" } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "schedule_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Overrides the campaign's schedule; `null` sends now." } } } } } }, "responses": { "202": { "description": "`Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } }, "required": [ "data" ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Pause a campaign (/docs/api/campaigns/pause-a-campaign) Only queued or sending campaigns can be paused; any other status is `409 invalid_state`. ## POST /api/v1/campaigns/{id}/pause ```json { "operationId": "pause-a-campaign", "description": "Only queued or sending campaigns can be paused; any other status is\n`409 invalid_state`.", "summary": "Pause a campaign", "tags": [ "Campaigns" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`Campaign`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List campaign recipients (/docs/api/campaigns/list-campaign-recipients) Newest first, with each recipient's delivery status. ## GET /api/v1/campaigns/{id}/recipients ```json { "operationId": "list-campaign-recipients", "description": "Newest first, with each recipient's delivery status.", "summary": "List campaign recipients", "tags": [ "Campaigns" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `CampaignRecipient`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/CampaignRecipient" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Send a message (/docs/api/messages/send-a-message) Send a WhatsApp text, approved template, interactive message, Flow, or media message to a contact (`contact_id`) or directly to an E.164 number (`to`). Send an `Idempotency-Key` header (or `client_message_id`) to retry safely: a retry returns the original message with `Idempotent-Replayed: true`. ## POST /api/v1/messages ```json { "operationId": "send-a-message", "description": "Send a WhatsApp text, approved template, interactive message, Flow, or\nmedia message to a contact (`contact_id`) or directly to an E.164\nnumber (`to`). Send an `Idempotency-Key` header (or `client_message_id`)\nto retry safely: a retry returns the original message with\n`Idempotent-Replayed: true`.", "summary": "Send a message", "tags": [ "Messages" ], "parameters": [ { "name": "Idempotency-Key", "in": "header", "description": "Any unique string up to 255 characters, such as a UUID. Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of acting twice.", "schema": { "type": "string" }, "example": "9f1c2a7e-4b8d-4e57-a3f0-6c1d2e3f4a5b" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/SendMessageRequest" } } } }, "responses": { "201": { "description": "`SentMessage`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/SentMessage" } }, "required": [ "data" ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List conversations (/docs/api/conversations/list-conversations) Most recently active first. Filter by `channel`, `status` and `assigned_to` (a user id). ## GET /api/v1/conversations ```json { "operationId": "list-conversations", "description": "Most recently active first. Filter by `channel`, `status` and\n`assigned_to` (a user id).", "summary": "List conversations", "tags": [ "Conversations" ], "parameters": [ { "name": "channel", "in": "query", "schema": { "type": "string", "enum": [ "" ] } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "" ] } }, { "name": "assigned_to", "in": "query", "description": "User id of the assigned agent.", "schema": { "type": "integer", "minimum": 1 } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Conversation`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Conversation" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Retrieve a conversation (/docs/api/conversations/retrieve-a-conversation) ## GET /api/v1/conversations/{id} ```json { "operationId": "retrieve-a-conversation", "summary": "Retrieve a conversation", "tags": [ "Conversations" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`Conversation`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Conversation" } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # List conversation messages (/docs/api/conversations/list-conversation-messages) Oldest first. ## GET /api/v1/conversations/{id}/messages ```json { "operationId": "list-conversation-messages", "description": "Oldest first.", "summary": "List conversation messages", "tags": [ "Conversations" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }, { "name": "limit", "in": "query", "description": "Items per page, 1 to 100.", "schema": { "type": "integer", "default": 25 } }, { "name": "cursor", "in": "query", "description": "`meta.next_cursor` (or `meta.prev_cursor`) of the previous page.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Paginated set of `Message`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Message" } }, "meta": { "type": "object", "properties": { "limit": { "type": "integer" }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "prev_cursor": { "type": [ "string", "null" ] } }, "required": [ "limit", "has_more", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Message volume (/docs/api/analytics/message-volume) Messages per channel between `from` and `to` (dates, `YYYY-MM-DD`; default the last 30 days, at most 366 days). ## GET /api/v1/analytics/messages ```json { "operationId": "message-volume", "description": "Messages per channel between `from` and `to` (dates, `YYYY-MM-DD`;\ndefault the last 30 days, at most 366 days).", "summary": "Message volume", "tags": [ "Analytics" ], "parameters": [ { "name": "from", "in": "query", "description": "First day, `YYYY-MM-DD`. Defaults to 29 days ago.", "schema": { "type": "string", "format": "date" }, "example": "2026-10-01" }, { "name": "to", "in": "query", "description": "Last day, `YYYY-MM-DD`. Defaults to today.", "schema": { "type": "string", "format": "date" }, "example": "2026-10-22" } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } }, "meta": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" } }, "required": [ "from", "to" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Conversation activity (/docs/api/analytics/conversation-activity) Conversations resolved over time and the channel mix between `from` and `to` (same range rules as message volume). ## GET /api/v1/analytics/conversations ```json { "operationId": "conversation-activity", "description": "Conversations resolved over time and the channel mix between `from`\nand `to` (same range rules as message volume).", "summary": "Conversation activity", "tags": [ "Analytics" ], "parameters": [ { "name": "from", "in": "query", "description": "First day, `YYYY-MM-DD`. Defaults to 29 days ago.", "schema": { "type": "string", "format": "date" }, "example": "2026-10-01" }, { "name": "to", "in": "query", "description": "Last day, `YYYY-MM-DD`. Defaults to today.", "schema": { "type": "string", "format": "date" }, "example": "2026-10-22" } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "over_time": { "type": "array", "items": { "type": "string" } }, "channel_mix": { "type": "array", "items": {} } }, "required": [ "over_time", "channel_mix" ] }, "meta": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" } }, "required": [ "from", "to" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/ValidationFailed" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ``` # Campaign funnel (/docs/api/analytics/campaign-funnel) Sent, delivered, read and failed counts of one campaign, delivery over time and the most common failure reasons. ## GET /api/v1/analytics/campaigns/{campaignId}/funnel ```json { "operationId": "campaign-funnel", "description": "Sent, delivered, read and failed counts of one campaign, delivery over\ntime and the most common failure reasons.", "summary": "Campaign funnel", "tags": [ "Analytics" ], "parameters": [ { "name": "campaignId", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "funnel": { "type": "array", "items": {} }, "delivery_over_time": { "type": "array", "items": {} }, "failed_reasons": { "type": "array", "items": {} } }, "required": [ "funnel", "delivery_over_time", "failed_reasons" ] } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } ```