# Blueticked API — full reference for coding agents > Blueticked is a multi-channel messaging platform (WhatsApp, SMS, email) with contacts, campaigns, automations, AI agents, forms, published sites and commerce, for South African businesses. One REST API, one send endpoint, three channels. This document is generated from the same source as the Blueticked OpenAPI specification, so it cannot describe behaviour the API does not have. It is written to be read in full by a coding agent before writing an integration. ## Overview - Base URL: https://app.blueticked.com - All endpoints are prefixed `/api/v1` (the version lives in the path, not a header). - Request and response bodies are JSON. Send `Content-Type: application/json` on writes. - The API is versioned in the path (/api/v1). Additive changes — new endpoints, new optional fields, new response fields — ship without a version bump, so parse responses leniently. Breaking changes ship under a new version prefix. - OpenAPI 3.1 document (authoritative, machine-readable, includes every request/response schema): https://app.blueticked.com/api/v1/openapi.json - TypeScript/JavaScript: `npm install @blueticked/sdk` (typed, zero dependencies). Any other language: generate a client from the OpenAPI document above, or call the REST API directly. ### MCP server If your client supports the Model Context Protocol, you can call this API as tools instead of writing HTTP by hand: ``` claude mcp add --transport http --scope user blueticked https://app.blueticked.com/api/mcp ``` It is a remote server — nothing to install. The tools run as the key in that browser-approved OAuth grant and remain bound by its API scopes, business restrictions and rate limits. No API key is copied into the coding project. `list_endpoints` and `call_api` cover anything the typed tools do not. Call `get_workspace_context` first so the agent discovers its organization, allowed businesses, test/live environment and scopes instead of inferring them from page text or failed requests. ## Quickstart 1. **Create a test key** — In Developers → API keys, create a blu_test_ key. Test keys sandbox every send end to end: the message row walks its real lifecycle, no provider is ever called, and nothing bills. A test key works on a brand-new workspace — you do not need to connect WhatsApp, a sender number or a mailbox first. 2. **Add a contact** — POST /api/v1/contacts with a phone number, or sync one from your system. Sends target existing contacts. 3. **Send your first message** — POST /api/v1/messages with a `body` — no template, no channel setup, nothing to connect first. On a blu_test_ key the whole lifecycle is simulated (queued → sent → delivered → read for WhatsApp; SMS and email stop at delivered, because neither emits a read receipt live). No provider is called and nothing bills. 4. **Test failure and inbound, not just the happy path** — The sandbox has deterministic magic numbers, so you can build your error handling before you ever go live: +27800000001 delivers (and so does any other number), +27800000002 fails at the send step with `recipient_unreachable`, and +27800000003 delivers and then fires a simulated inbound reply — the same webhook shape a real customer reply produces. 5. **Going live: WhatsApp templates** — Sandbox sends accept a free-form body on every channel. Live WhatsApp is different, and that is Meta's rule rather than ours: a business-initiated WhatsApp message needs a template Meta has approved, and a free-form body is only valid inside an open 24-hour customer-service window. SMS and email have no such restriction. Approve a template before you switch to a blu_live_ key, or your first live send will be the first thing that fails. 6. **Go live with idempotency** — Swap in a blu_live_ key and send one real message with an Idempotency-Key header so retries never double-send. (test_mode: true stays available on any key as a dry-run that validates without sending.) 7. **Subscribe a webhook** — Create a webhook in Developers → Webhooks, fire a signed test delivery, and verify the signature against the raw body. ## Going live (the provisioning journey) Calling the API is the easy part. What stalls an integration is provisioning, and most of it is performed by a HUMAN at the customer's business: 1. **Build entirely in the sandbox.** A `blu_test_` key works on a workspace with NO channel connected and nothing funded. Sends are simulated, nothing is billed. Do not wait for provisioning to start building. 2. **Get the customer's WhatsApp connected.** They must complete Meta's Embedded Signup themselves — no API key can do it for them. Mint a link with `POST /api/v1/channels/connect-sessions` and send it to them. Single use, expires in 24h, needs no Blueticked account. 3. **Wait for Meta** — business verification and display-name review. HOURS TO DAYS, and nothing shortens the queue. Start it as early as possible. 4. **Detect readiness without asking the customer.** Either subscribe to the `channel.connected` webhook (it echoes the `reference` you set when minting the link) or poll `GET /api/v1/readiness`, which returns `can_send` per channel plus `blockers[]`. Each blocker carries a stable `code` and a `resolve` of `dashboard`, `api` or `meta_review`. Branch on `resolve`: `dashboard` means a person must act — surface it, do NOT retry. 5. **Get a template approved.** Business-initiated WhatsApp requires one, and Meta reviews it. Submit early, usually while step 3 is still queued. 6. **Fund the prepaid ZAR wallet**, then swap `blu_test_` for `blu_live_`. Nothing else in your code changes. Order matters: the only slow steps are the two Meta owns, so begin them first and write code while they queue. Never block your own onboarding flow on them finishing. ## Authentication ### Bearer auth Send Authorization: Bearer blu_live_… (or blu_test_… for the sandbox) on every /api/v1 request. The full key is shown once at creation — store it in a secrets manager. ### Scoped keys Each key grants a fixed set of scopes; a missing scope returns 403 forbidden. Grant only what an integration needs, and use a separate key per system. ### IP allowlist Restrict a key to trusted IPs or IPv4/IPv6 CIDR ranges from Developers → API keys. Requests from other sources are refused with 403. ### Rate limits Each key allows 120 requests per minute by default. rate_limited (429) responses carry x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset headers — back off until reset. Key prefixes are meaningful and stable: - `blu_live_…` — a live key. Sends reach real recipients and spend credit. - `blu_test_…` — a sandbox key. See Testing below. - `bto_at_…` — an OAuth access token from a browser-approved connection (an MCP client, or a third-party app's "Connect Blueticked" flow). It works on every endpoint exactly like a key: the grant holds a delegated key server-side, and its scopes, business restrictions and rate limits apply unchanged. Your OAuth client refreshes it automatically; a 401 telling you to reconnect means the grant itself was revoked or expired. The full key is displayed once, at creation. It cannot be retrieved again; rotate the key if it is lost. Never place a key in client-side code, a query string, or a committed file. ## Scopes Every key carries a fixed scope set; a missing scope is `403 forbidden`. Sends accept either the umbrella `messages:send` or the per-channel scope (`whatsapp:send`, `sms:send`, `email:send`) — the per-channel one is the least-privilege choice for a single-channel integration. - `workspace:read` — Read the organization, accessible businesses and effective credential permissions. - `businesses:read` — List the businesses in this workspace. A platform reads this to reconcile its own customer records against ours. - `businesses:write` — Create a business and mint a credential scoped to it — how a platform onboards its own customers without a human in our dashboard. Powerful: grant it only to the one organization-wide key that does onboarding, never to the per-customer keys it issues. - `messages:send` — Send WhatsApp, SMS and email messages — the umbrella send scope. Use a per-channel scope below to grant just one. - `whatsapp:send` — Send WhatsApp messages only — a least-privilege alternative to messages:send for WhatsApp-only integrations. - `sms:send` — Send SMS only — a least-privilege alternative to messages:send for SMS-only integrations. - `email:send` — Send email only — a least-privilege alternative to messages:send for email-only integrations. - `events:send` — Trigger Blueticked automations from booking systems, CRMs, websites and other apps. - `messages:read` — Fetch the status and content of messages. - `contacts:read` — List and fetch contacts in your workspace. - `contacts:write` — Create and update contacts. - `commerce:read` — List products, stock levels and orders. - `commerce:write` — Create and update products, adjust stock, mark orders fulfilled. - `campaigns:read` — List campaigns and inspect delivery counters. - `campaigns:write` — Create, schedule and queue campaign sends. - `templates:read` — List WhatsApp templates and approval statuses. - `templates:write` — Create and submit WhatsApp templates for Meta approval. - `channels:read` — Inspect connected channels and WhatsApp launch readiness. - `channels:write` — Configure channel settings such as the email sending identity. - `flows:read` — List WhatsApp Flows and reusable automation bundles. - `flows:write` — Create and publish WhatsApp Flows. Reserved for the Flows API. - `automations:read` — List reply automations and read their definitions, status and run counts. - `automations:write` — Create reply automations as DRAFTS from recipes and pause live ones. Drafts never message anyone — a human activates them in the dashboard. - `ai_agents:read` — List AI agents and read their personas, autonomy settings and channel bindings. - `ai_agents:write` — Stop an AI agent replying — the safe direction. Configuring or re-enabling one stays a dashboard job. - `journeys:read` — List journeys (sequences) and read their steps, status and enrolment counts. - `journeys:enroll` — Put contacts onto a live journey. Send-class: a journey messages people over time, so this sits outside the Prepare profile. - `sites:read` — List the workspace's sites with their slugs, statuses and live addresses. - `sites:write` — Work on sites without changing what the public sees — including taking a live site offline. Publishing needs sites:publish. - `sites:publish` — Put a site's draft live on the public internet. Deliberately separate from sites:write so a Prepare-profile agent can never publish. - `forms:read` — List what visitors submitted through site forms — the leads intake. - `forms:write` — Flip a submission's read state after an external system ingests it. - `conversations:read` — List conversations and inspect the message history of each. - `conversations:write` — Assign, resolve and reopen conversations. - `suppression:read` — List opted-out identifiers (do-not-contact entries). - `suppression:write` — Add and remove do-not-contact entries. - `webhooks:read` — List webhook subscriptions and inspect delivery history. - `webhooks:write` — Create, update, test and delete webhook subscriptions. - `integrations:read` — List outbound integrations and inspect their delivery history. - `integrations:write` — Create, update, delete and test-fire outbound HTTP integrations. - `conversions:write` — Report conversions/revenue (ANL-01) so campaigns and journeys can attribute them to email engagement. - `wallet:read` — Read the prepaid wallet balance and transaction ledger. ## Testing Two independent mechanisms, often confused. They are not the same thing: 1. **Test API keys** (`blu_test_…`) — a full sandbox. A message row is created and advanced queued → sent → delivered → read (~5s per step). No provider is called and no credit is spent. Build the entire integration against one of these. 2. **`test_mode: true`** (a field in the request body) — a dry run. The request is validated and the response comes back with `status: "validated"`; nothing is created and nothing is sent. Works with any key, including a live one. Sandbox numbers (test keys only) drive deterministic outcomes: - `+27800000001` — Always delivers - `+27800000002` — Fails at send with recipient_unreachable - `+27800000003` — Delivers, then fires a simulated message.received reply Webhook deliveries triggered by a test key carry `"test": true`. ## Request conventions ### Pagination List endpoints are cursor-paginated. Pass ?limit= to size a page (limits vary by endpoint — see each endpoint's reference) and page forward with ?cursor= using the previous response's next_cursor. When has_more is false you have reached the end. Treat cursors as opaque tokens — pass back exactly what a response returns; do not synthesise them. ### Idempotency Send an Idempotency-Key header (a UUID) on POST writes so a retried request never repeats a send. Replays within 24 hours return the original response. Reusing a key with a different body returns 409 idempotency_conflict; a concurrent duplicate returns 409 idempotency_in_progress with retry-after. ### Identifiers - Phone numbers are E.164, including the country code (`+27821234567`). A local-format number is rejected with `validation_failed`. - `business_id` scopes a request to one business inside the workspace. A key restricted to specific businesses returns `403 forbidden` when the field is missing or names a business the key cannot reach. If the workspace has more than one business, send it explicitly. - Cursors are opaque. Pass back exactly the `next_cursor` a response returned; never construct, decode or increment one. ## Endpoints Format: `METHOD /path` — summary _(required scope)_. Full request/response schemas for every entry are in the OpenAPI document. - `GET /api/v1/me` — Read the authenticated credential's organization, accessible businesses and effective permission boundary. _(workspace:read)_ - `GET /api/v1/readiness` — Find out whether this workspace can actually send yet, and what is standing in the way — as branchable data rather than a failed send. _(workspace:read)_ - `GET /api/v1/businesses` — List the businesses in this workspace — one per customer you have onboarded. _(businesses:read)_ - `POST /api/v1/businesses` — Create a business for one of your customers, and optionally mint a credential scoped to it in the same call. _(businesses:write)_ - `POST /api/v1/businesses/{id}/keys` — Mint an API key restricted to one existing business — for rotation, or a second narrower key. _(businesses:write)_ - `POST /api/v1/messages` — Send one WhatsApp template, SMS, or email message to an existing contact. _(messages:send)_ - `POST /api/v1/messages` — Send a WhatsApp template that has a DOCUMENT header, attaching a per-recipient PDF by URL. _(messages:send)_ - `POST /api/v1/messages` — Send a WhatsApp template whose URL button ends in {{1}}, supplying the per-send link ending. _(messages:send)_ - `POST /api/v1/messages` — Send a plain-text SMS to an existing contact, from a free-form body or a saved message template. _(messages:send)_ - `POST /api/v1/messages` — Send a transactional email to an existing contact, with an optional subject line and reply-to. _(messages:send)_ - `POST /api/v1/messages` — Send a marketing email with your own HTML body, rendered inside the branded shell with the required compliance footer. _(messages:send)_ - `POST /api/v1/messages` — Validate auth, contact lookup, channel readiness, template variables and opt-outs without sending. _(messages:send)_ - `GET /api/v1/messages/{id}` — Fetch the latest status and provider identifiers for a previously queued message. _(messages:read)_ - `POST /api/v1/events/send` — Trigger a saved Blueticked automation from a booking system, CRM, website form or other external app. _(events:send)_ - `POST /api/v1/contacts` — Create a contact or update the existing contact matched by phone or email. _(contacts:write)_ - `GET /api/v1/contacts` — Cursor-paginated contact list with tag, group and free-text filters. _(contacts:read)_ - `GET /api/v1/contacts/{id}` — Fetch a single contact by id. _(contacts:read)_ - `PATCH /api/v1/contacts/{id}` — Partially update a contact's profile, tags, or suppressed flag. _(contacts:write)_ - `DELETE /api/v1/contacts/{id}` — Soft-delete a contact so it drops out of lists, audiences and sends. _(contacts:write)_ - `POST /api/v1/contacts/{id}/erase` — Anonymise a contact and its personal data for a POPIA erasure request. _(contacts:write)_ - `GET /api/v1/groups` — Static contact lists with live member counts — the audiences campaigns can target. _(contacts:read)_ - `POST /api/v1/groups` — Create a static contact list, then fill it via the members endpoint. _(contacts:write)_ - `GET /api/v1/groups/{id}` — Fetch one contact group with its live member count. _(contacts:read)_ - `PATCH /api/v1/groups/{id}` — Rename a contact group or edit its description. _(contacts:write)_ - `DELETE /api/v1/groups/{id}` — Soft-delete a contact group and clear its membership rows. _(contacts:write)_ - `PUT /api/v1/groups/{id}/members` — Add up to 500 contacts to a group in one call. _(contacts:write)_ - `DELETE /api/v1/groups/{id}/members` — Remove contacts from a group by id. _(contacts:write)_ - `GET /api/v1/segments` — Read-only smart audiences — pass a segment id to campaign sends; membership is evaluated at send time. _(contacts:read)_ - `GET /api/v1/conversations` — Read conversation state for inbox, CRM sync, or support tooling. _(conversations:read)_ - `PATCH /api/v1/conversations/{id}` — Assign, resolve, or reopen a conversation from support tooling. _(conversations:write)_ - `GET /api/v1/conversations/{id}` — Fetch a single conversation's state. _(conversations:read)_ - `GET /api/v1/conversations/{id}/messages` — Read the message history (inbound and outbound) for one conversation, newest first. _(conversations:read)_ - `POST /api/v1/campaigns` — Create, schedule, validate, or queue a bulk WhatsApp campaign. _(campaigns:write)_ - `GET /api/v1/campaigns` — Fetch campaign status, counters, filters and cursor pagination. _(campaigns:read)_ - `GET /api/v1/campaigns/{id}` — Fetch one campaign's status and delivery counters. _(campaigns:read)_ - `POST /api/v1/templates` — Submit a WhatsApp template for Meta approval through a connected WABA. _(templates:write)_ - `GET /api/v1/templates` — List WhatsApp templates and their approval status, with cursor pagination. _(templates:read)_ - `GET /api/v1/templates/{id}` — Fetch one template, including its body, buttons, and rejection reason if any. _(templates:read)_ - `POST /api/v1/flows` — Create a WhatsApp Flow draft from a definition in Blueticked's builder dialect. _(flows:write)_ - `POST /api/v1/flows/{id}/publish` — Publish a valid WhatsApp Flow draft to Meta. _(flows:write)_ - `GET /api/v1/flows` — List WhatsApp Flows and their publish status, with cursor pagination. _(flows:read)_ - `GET /api/v1/flows/{id}` — Fetch one Flow, including its full definition. _(flows:read)_ - `PATCH /api/v1/flows/{id}` — Update a Flow's name, description, or definition. _(flows:write)_ - `DELETE /api/v1/flows/{id}` — Soft-delete a Flow so it no longer appears in the workspace. _(flows:write)_ - `GET /api/v1/automations` — Every reply automation in the workspace with its channel, trigger, status and run count. _(automations:read)_ - `GET /api/v1/automations/recipes` — The named starting points an automation can be created from — each a reviewed, working definition. _(automations:read)_ - `POST /api/v1/automations` — Instantiate a recipe as a PAUSED automation for a human to review and activate in the dashboard. _(automations:write)_ - `GET /api/v1/automations/{id}` — One automation in full, definition included. _(automations:read)_ - `PATCH /api/v1/automations/{id}` — Stop a live automation — the safe direction, always available to an agent. _(automations:write)_ - `GET /api/v1/journeys` — Every journey with its status, step count and enrolment trigger. _(journeys:read)_ - `GET /api/v1/journeys/{id}` — One journey in full, its raw steps included. _(journeys:read)_ - `POST /api/v1/journeys/{id}/enroll` — Put contacts onto an active journey — they start receiving its programme from step one. _(journeys:enroll)_ - `GET /api/v1/ai-agents` — Every AI agent with its autonomy mode, channels and thresholds — the review surface. _(ai_agents:read)_ - `GET /api/v1/ai-agents/{id}` — One agent in full, its persona text included. _(ai_agents:read)_ - `PATCH /api/v1/ai-agents/{id}` — Stop an agent replying immediately — the always-available safe direction. _(ai_agents:write)_ - `GET /api/v1/sites` — Every site in the workspace with its slug, live status and public address. _(sites:read)_ - `POST /api/v1/sites` — Create a draft site — from a house template or the blank starter — ready for the studio. _(sites:write)_ - `POST /api/v1/sites/{id}/publish` — Put the site's current draft live on the public internet — validated, image-audited, cache-busted. _(sites:publish)_ - `POST /api/v1/sites/{id}/unpublish` — Flip a published site back to draft — every hostname stops serving once caches clear. _(sites:write)_ - `GET /api/v1/form-submissions` — What visitors submitted through site forms, newest first — poll with unread=true as an intake queue. _(forms:read)_ - `PATCH /api/v1/form-submissions/{id}` — Flip the read flag once an external system has ingested it. _(forms:write)_ - `GET /api/v1/channels/whatsapp/readiness` — Check WABA connection, template, test send, reply and webhook readiness. _(channels:read)_ - `GET /api/v1/channels` — Inventory of connected channels and their connection status. _(channels:read)_ - `POST /api/v1/channels/connect-sessions` — Mint a single-use link that lets your customer connect their WhatsApp Business account themselves — no Blueticked login required. _(channels:write)_ - `GET /api/v1/channels/connect-sessions` — The connect links you have sent, newest first, with the status of each. _(channels:read)_ - `DELETE /api/v1/channels/connect-sessions/{id}` — Kill a link you have sent — wrong address, forwarded, or simply no longer wanted. _(channels:write)_ - `POST /api/v1/webhooks` — Subscribe an https endpoint to events. The signing secret is returned exactly once. _(webhooks:write)_ - `POST /api/v1/webhooks/{id}/test` — Fire a signed sample event (payload carries test: true) at the subscription. _(webhooks:write)_ - `GET /api/v1/webhook-deliveries` — Inspect signed webhook attempts, receiver responses, retry state, and final outcome. _(webhooks:read)_ - `POST /api/v1/webhook-deliveries/{id}/replay` — Create and queue a fresh attempt for an existing delivery after the receiver has been fixed. _(webhooks:write)_ - `GET /api/v1/webhooks` — List the workspace's webhook subscriptions. Secrets are never returned. _(webhooks:read)_ - `PATCH /api/v1/webhooks/{id}` — Update a subscription's url, subscribed events, or active flag. _(webhooks:write)_ - `PATCH /api/v1/webhooks/{id}` — Issue a new signing secret for a webhook subscription. The new secret is returned once. _(webhooks:write)_ - `DELETE /api/v1/webhooks/{id}` — Remove a webhook subscription. No further deliveries are attempted. _(webhooks:write)_ - `GET /api/v1/integrations` — List outbound HTTP integrations, newest first, with cursor pagination. _(integrations:read)_ - `POST /api/v1/integrations` — Create an outbound HTTP integration. It starts paused; test-fire it, then resume to go live. _(integrations:write)_ - `GET /api/v1/integrations/{id}` — Fetch a single integration's public configuration and health. _(integrations:read)_ - `PATCH /api/v1/integrations/{id}` — Update any subset of an integration's fields, including its live status. _(integrations:write)_ - `DELETE /api/v1/integrations/{id}` — Soft-delete an integration and stop it firing. _(integrations:write)_ - `GET /api/v1/integrations/{id}/deliveries` — List an integration's delivery attempts (test + live), newest first, with cursor pagination. _(integrations:read)_ - `POST /api/v1/integrations/{id}/test` — Send a signed test delivery to the endpoint. On success the integration is switched to active. _(integrations:write)_ - `GET /api/v1/suppression` — List do-not-contact entries (opt-outs), newest first, with cursor pagination. _(suppression:read)_ - `POST /api/v1/suppression` — Add a do-not-contact entry. Idempotent: re-adding returns the existing entry. _(suppression:write)_ - `DELETE /api/v1/suppression/{id}` — Remove a do-not-contact entry so the identifier becomes contactable again. _(suppression:write)_ - `GET /api/v1/email/sending-domain` — Read a business email sending identity and its DNS verification status. _(channels:read)_ - `POST /api/v1/email/sending-domain` — Register a Blueticked-managed sending identity and get back the DNS records to add. Emails use that sender once it verifies. _(channels:write)_ - `POST /api/v1/email/sending-domain/verify` — Trigger a DNS re-check now. When it returns verified, every send uses the verified sender address. _(channels:write)_ - `DELETE /api/v1/email/sending-domain` — Remove the custom sending identity and revert the workspace to the shared Blueticked sender. _(channels:write)_ - `GET /api/v1/wallet` — Read the organization's prepaid wallet balance in ZAR cents and in credits. _(wallet:read)_ - `GET /api/v1/wallet/ledger` — Read the append-only wallet transaction history, newest first, with cursor pagination. _(wallet:read)_ - `POST /api/v1/conversions` — Report a conversion (an order, booking or any success event). Blueticked attributes it to the contact's most recent email click — falling back to the most recent delivery — within a 7-day window, and rolls the value up on the campaign and reports surfaces. _(conversions:write)_ - `GET /api/v1/commerce/products` — List the shop's products with live stock levels, newest first (up to 200). _(commerce:read)_ - `POST /api/v1/commerce/products` — Add a product to the shop. _(commerce:write)_ - `POST /api/v1/commerce/products/media` — Upload a photo and get back a hosted URL to set as a product's `image_url`. _(commerce:write)_ - `PATCH /api/v1/commerce/products/{id}` — Change any product field — the stock-sync call: set `stock_quantity` after a delivery arrives or a till sale happens elsewhere. _(commerce:write)_ - `DELETE /api/v1/commerce/products/{id}` — Soft-delete a product: it leaves the shop but stays behind old order lines. _(commerce:write)_ - `GET /api/v1/commerce/orders` — The order book, newest first — what sold, to whom, and where each order stands. _(commerce:read)_ - `GET /api/v1/commerce/orders/overview` — The order book's headline numbers — counts, revenue and last sale — computed over every order in one call. _(commerce:read)_ - `GET /api/v1/commerce/orders/{id}` — Fetch a single order in full — the re-fetch half of the webhook contract. _(commerce:read)_ - `PATCH /api/v1/commerce/orders/{id}/fulfilment` — Mark a paid order handed over (or undo it) — the call a warehouse or fulfilment system makes when goods leave. _(commerce:write)_ - `GET /api/v1/commerce/payment-links` — The 25 most recent payment links — each an order with a Paystack checkout URL, with its paid/pending state. _(commerce:read)_ - `POST /api/v1/commerce/payment-links` — Create a Paystack-hosted checkout link for one amount (or priced items) and send it over any channel. _(commerce:write)_ - `GET /api/v1/commerce/fulfilment` — How the shop currently offers collection and delivery, with fees. _(commerce:read)_ - `PUT /api/v1/commerce/fulfilment` — Configure collection and delivery for checkout — the settings an agent needs before a shop can sell. _(commerce:write)_ - `GET /api/v1/commerce/discounts` — Every discount code with its derived redemption count — counted from paid orders only. _(commerce:read)_ - `POST /api/v1/commerce/discounts` — Create a checkout discount: a percentage, a rand amount, or free delivery — exactly one of the three. _(commerce:write)_ - `PATCH /api/v1/commerce/discounts/{id}` — Stop a code redeeming (or turn it back on) without deleting it. _(commerce:write)_ - `DELETE /api/v1/commerce/discounts/{id}` — Remove a code for good — safe because orders copy the code as text. _(commerce:write)_ ## Errors Failures return a JSON envelope with a stable machine-readable code: ```json { "error": { "code": "validation_failed", "message": "to must be a valid E.164 phone number", "details": { "field": "to" } }, "request_id": "req_01H…" } ``` Branch on `code`, never on `message` — messages are prose and may be reworded. Include `request_id` when reporting a problem; it identifies the exact request in the workspace's API log. - `unauthorized` — Missing key, revoked key, or malformed Authorization header. - `forbidden` — The API key exists but does not have the required scope. - `not_found` — The requested contact, template, message or campaign does not exist. - `validation_failed` — The request shape is valid JSON but fails business validation. - `rate_limited` — The workspace or key exceeded a rate limit. - `idempotency_conflict` — The same Idempotency-Key was reused with a different body. - `idempotency_in_progress` — A request with the same Idempotency-Key is still being processed; retry once it finishes. - `resource_in_use` — The resource cannot be deleted because other live resources still depend on it (e.g. a group targeted by a scheduled campaign). `details` lists the dependents. - `method_not_allowed` — The endpoint does not support the attempted HTTP method. (Next.js answers unsupported methods with a bodyless 405, so this code is listed for completeness rather than returned in the JSON envelope.) - `channel_unavailable` — The requested provider channel is disconnected or not ready. - `queue_unavailable` — The send queue is not configured, so live sends cannot be accepted. - `insufficient_balance` — The prepaid wallet does not have enough available credit for the requested send. - `spend_cap_reached` — This API key has reached the monthly spend cap set on it. The workspace wallet is unaffected — raise or remove the cap in Developers → API keys. Do not retry: the stop is deliberate and only a human can lift it. - `plan_limit_reached` — A per-tier plan cap (seats, sites, contacts or WhatsApp numbers) is exhausted. `details` carries { key, tier, limit, current }. Maps to 403. - `subscription_inactive` — The organization's subscription is suspended or cancelled, so the entitlement-gated action is refused. Maps to 403. - `variables_mismatch` — The supplied template variables do not match the template's {{n}} markers (missing or extra positions). - `button_params_mismatch` — The supplied template_buttons do not match the template's dynamic URL buttons — a stored button URL ending in {{1}} needs a url_suffix on every send. - `document_invalid` — The document parameter is malformed: url must be https and filename at most 240 characters. - `document_required` — The template has a DOCUMENT header; pass document: { url, filename }. - `document_not_applicable` — A document was supplied but the template has no DOCUMENT header (or the send is not a template send). - `document_source_invalid` — The campaign document_source is malformed; check mode, field_key, filename_template and missing_policy. - `document_source_required` — The campaign template has a DOCUMENT header; pass a document_source. - `document_source_not_applicable` — A document_source was supplied but the campaign is neither an email campaign nor a WhatsApp template with a DOCUMENT header. - `internal_error` — Blueticked accepted the request but hit an internal failure. - `payload_too_large` — The request body is above the endpoint's size limit; returned as 413 before the body is validated. - `service_unavailable` — Blueticked could not verify your workspace state; the request was not applied, so retry shortly. - `provider_error` — An upstream provider refused or failed the request — Paystack declining a payment link, for instance. Returned as 502. Unlike service_unavailable this usually will not fix itself: check the request and the provider account connected in your workspace before retrying. - `enable_requires_dashboard` — The API can only pause an AI agent. Re-enabling one is a dashboard action, where its persona and escalation rules are visible. - `activation_requires_dashboard` — The API can only pause an automation. Activating one is a dashboard action, so nobody ships a live automation sight-unseen. - `journey_not_active` — Only an active journey accepts enrolments; a draft, paused or archived one is refused. Activation is a dashboard action. ## Connected apps (the "Connect Blueticked" pattern) If you are a SaaS whose customers also use Blueticked, offer them a Connect Blueticked button instead of asking them to paste an API key: 1. **Register once** via OAuth dynamic client registration (`POST /api/oauth/register` — public, no credentials). Your `client_name` is what users see on the consent screen and in their connected-apps list. 2. **Connect per customer**: run the standard OAuth 2.1 + PKCE flow (discovery documents are published under `/.well-known/`). The customer approves the organization, businesses, an access profile, Test or Live, and an expiry. Your access token (`bto_at_…`) works on EVERY `/api/v1` endpoint, is refreshed by your client, and is revocable by the customer at any time. 3. **Provision a starter kit — as drafts.** Right after connecting, use the grant to set up everything your integration needs: draft templates (`POST /api/v1/templates`), automations from `api_event` recipes (`POST /api/v1/automations` — these are ALWAYS created paused; only a human in the dashboard can enable them), and your webhook subscription (`POST /api/v1/webhooks`). Then send the customer to their Blueticked dashboard to review and enable. One click per automation, full visibility, nothing activates silently. 4. **Run on events, not messages.** Emit what happened (`POST /api/v1/events` with your own event names and data payload); the customer's automations decide what gets sent, using your event fields as template variables. They keep full power to reshape the messaging in Blueticked without you shipping anything. 5. **Listen on webhooks** for delivery outcomes and inbound replies, and write them back into your product. Re-run step 3 idempotently: list what exists before creating, and never recreate something the customer deleted — a deleted draft is an answer, not an oversight. ## Handling failure (read this before writing a retry loop) Every error body carries `error.next_steps` — an array of remedies, as data. Branch on them; never parse `error.message`. ```json { "ok": false, "error": { "code": "channel_unavailable", "message": "No WhatsApp channel is registered for this workspace.", "next_steps": [ { "action": "connect_channel", "by": "human", "description": "Mint a connect link and send it to the account owner.", "endpoint": "POST /api/v1/channels/connect-sessions" } ] }, "request_id": "req_abc123" } ``` `by` is the field that matters: - `caller` — you can fix this in code. Correct the request and retry. - `human` — a PERSON must act in a browser. **Do not retry**: no rewrite of this request will succeed. Surface it to your operator or your customer. - `waiting` — outside anyone's control (an upstream review or outage). Poll or back off. The common mistake this exists to prevent: treating `503 channel_unavailable` as transient and retrying. It is a `human` step — nobody has connected WhatsApp yet — so every retry fails identically. ### Rehearsing failures before production A sandbox key can ask for a specific failure, so error handling is testable instead of first met against a real customer: ``` X-Simulate-Error: channel_unavailable ``` The response is the real one — real status, real code, real `next_steps` — with `error.details.simulated: true` so a test can assert it took effect. Send an unrecognised code and the error lists every code you may ask for. Live keys REFUSE the header rather than ignoring it: a suite that appeared to pass against a live key would be proving nothing. ## Webhooks Blueticked POSTs a signed JSON envelope to your endpoint: ```json { "event": "message.delivered", "delivery_id": "whd_9f8e7d6c", "organization_id": "org_123", "timestamp": "2026-07-13T10:30:00.000Z", "data": { "message_id": "msg_abc123", "business_id": "22222222-2222-4222-9222-222222222222", "channel": "whatsapp", "status": "delivered", "external_id": "wamid.HBg...", "occurred_at": "2026-07-13T10:30:00.000Z" } } ``` ### Verifying the signature - Header: `x-blueticked-signature` - Algorithm: HMAC-SHA256, hex-encoded, prefixed `sha256=` - Signed content: the raw, unparsed request body — verify before JSON.parse, and never re-serialize. - Compare with a constant-time function (`crypto.timingSafeEqual`), not `===`. - During a secret rotation the previous signature is sent in `x-blueticked-signature-previous` for 24 hours — accept either. ### Delivery - Retries: 8 attempts, exponential, starting at 10 seconds. - Respond `2xx` quickly and process asynchronously; a slow handler is retried and will deliver the same event again. - Deliveries are at-least-once. De-duplicate on `delivery_id`. ### Events - `message.received` - `message.sent` - `message.delivered` - `message.read` - `message.failed` - `email.opened` - `email.clicked` - `email.bounced` - `email.complained` - `contact.created` - `contact.updated` - `suppression.created` - `suppression.removed` - `suppression.imported` - `flow.submitted` - `channel.connected` - `agent.run.completed` - `commerce.order.paid` - `commerce.order.fulfilled` - `commerce.order.refunded` - `commerce.order.abandoned` ## Common mistakes - Omitting `business_id` on a workspace with several businesses, or with a key restricted to specific businesses → `403 forbidden`. - Sending a local-format phone number instead of E.164 → `validation_failed`. - Business-initiated WhatsApp messages require an **approved template**. Free-form WhatsApp text is only valid inside an open 24-hour customer service window, opened by an inbound message from the customer. - Template variables must match the template's `{{n}}` markers exactly — a missing or extra position is `variables_mismatch`, not a silent skip. - Reusing an `Idempotency-Key` with a different body is `idempotency_conflict`. Generate a fresh UUID per logical operation, and reuse it only when retrying that same operation. - Verifying a webhook signature against a re-serialized body. Sign the raw bytes, before `JSON.parse`. - Treating `202 Accepted` on a send as delivery. It means queued; the outcome arrives on the `message.delivered` / `message.failed` webhook. - Polling a list endpoint for status. Subscribe to webhooks instead; the rate limit is per key and polling exhausts it. - A key can carry a monthly spend cap (set at creation via `monthly_spend_cap_rands`). At the cap, billable sends return `402 spend_cap_reached` until a human raises it — expected behaviour for an unattended agent's credential, not an error to retry. ## Where to go next - OpenAPI 3.1 schema: https://app.blueticked.com/api/v1/openapi.json - Human documentation: https://app.blueticked.com/developers/docs - Run a live request: https://app.blueticked.com/developers/console