Skip to content

Error codes

Every non-2xx response carries a machine-readable code, a human message, and next_steps — what to do about it, as data rather than prose. Branch on code and next_steps[].action; never parse the message.

Error shape
{
  "ok": false,
  "error": {
    "code": "insufficient_balance",
    "message": "This send needs more wallet credit before it can be queued.",
    "details": {
      "code": "insufficient_balance",
      "estimated_cost_zar_cents": 3400,
      "balance_zar_cents": 1200,
      "available_zar_cents": 1200
    },
    "next_steps": [
      {
        "action": "top_up_wallet",
        "by": "human",
        "description": "The prepaid ZAR wallet cannot cover this send. A person must top up. Sandbox sends are never billed.",
        "url": "https://app.blueticked.com/billing"
      }
    ]
  },
  "request_id": "req_abc123"
}

details is present on the codes that have something specific to say (the table below names them). next_steps is always present and never empty. Its by field is the one worth branching on:

  • caller — your code can fix this itself, right now.
  • human — a person must act in a browser. Surface it and stop retrying; no rewrite of the request will clear it.
  • waiting— outside anyone's control. Back off and retry.

The SDK throws a typed BluetickedError exposing code, status, details, and requestId — include the request id when contacting support.

Running out of credit

Blueticked billing is a prepaid ZAR wallet, so the most likely failure a long-running integration will meet is running out of money mid-run. Two distinct codes say so, and they are not interchangeable:

  • 402 insufficient_balance — the workspace wallet is short. details tells you by how much. Someone tops up and the same request succeeds.
  • 402 spend_cap_reached — the wallet is fine; this key's own monthly cap is spent. It is a deliberate guardrail, not a transient error. Never retry it — an unattended agent that treats it as retryable will hammer the endpoint until someone notices. Raise the cap in Developers → API keys, or wait for the month to roll.

Sandbox keys (blu_test_) never bill, so neither code can arise in test mode.

Every code

All 34codes the v1 API can return. “Cleared by” is the by value on the code's first next_steps entry.

CodeHTTPCleared byWhat it means, and what to do
invalid_field_map400Your codeThe incoming connection's field mapping is invalid. Correct the field mapping named by the error details, then retry.
unauthorized401Your codeMissing key, revoked key, or malformed Authorization header. Send a live or sandbox key as Authorization: Bearer. If the key was revoked or rotated, mint a new one.
forbidden403A personThe API key is valid but not permitted here — a missing scope, a blocked IP, a publishable key used off the agent chat endpoint, or an action the API refuses outright. For a missing scope, error.details carries granted and required, and someone with dashboard access must edit or recreate the key. Never retry unchanged.
not_found404Your codeThe requested contact, template, message or campaign does not exist. List the resource to find a valid id rather than retrying. An id belonging to another workspace also reports not found.
gone410Your codeThe endpoint was retired. It keeps answering 410, for at least one SDK major, with details.successor naming what replaced it. Move to the endpoint details.successor names; message says how. The same request will never succeed again, so do not retry it.
validation_failed400Your codeThe request shape is valid JSON but fails business validation. The message names the offending field. Fix it and retry; the same body will fail again.
payload_too_large413Your codeThe body is above the endpoint's size limit. Checked before the body is parsed, so nothing was applied. Split the request into smaller batches.
rate_limited429Your codeThe key or the workspace exceeded a per-minute limit. Wait until x-ratelimit-reset, then retry. error.details carries limit, reset_at and scope (key or organization).
idempotency_conflict409Your codeThe same Idempotency-Key was reused with a different body. Generate a fresh UUID per logical operation. Never reuse one across different payloads.
idempotency_in_progress409Your codeA request with the same Idempotency-Key is still being processed. Wait briefly and retry with the SAME key to collect the original result.
resource_in_use409Your codeThe resource cannot be deleted because live resources still depend on it — a group targeted by a scheduled campaign, for instance. error.details lists the dependents; remove or repoint those first.
conflict409Your codeThe other kind of 409: the request collides with what already exists — a code or name that is taken, or a resource in a state the call does not accept (only a paid order can book a courier). Not resource_in_use, where the resource is fine and other things depend on it: here there are no dependents to clear. Send a different value, or bring the resource into the required state, then retry. The same body will keep failing, and nobody has to open the dashboard for you.
method_not_allowed405Your codeThe endpoint does not support that HTTP method. Next.js answers unsupported methods with a bodyless 405, so this arrives without the JSON envelope. Check the endpoint's method in GET /api/v1/openapi.json.
enable_requires_dashboard400A personThe API can pause an AI agent but not enable one — switching on something that answers customers unattended needs a person looking at it. Surface this to your operator. Do not retry.
activation_requires_dashboard400A personThe API can pause an automation but not activate one. Automations are created paused by design. Surface this to your operator. Do not retry.
journey_not_active409A personOnly an active journey accepts enrolments; a draft, paused or archived one is refused. A person must activate the journey in the dashboard before this call can succeed.
channel_unavailable503A personThe requested provider channel is disconnected or not ready. Do not spin on it — retrying cannot succeed until somebody connects a channel. Mint a connect link with POST /api/v1/channels/connect-sessions and send it to the account owner; check GET /api/v1/readiness first. A blu_test_ key sends with no channel connected and bills nothing.
queue_unavailable503RetryThe send queue is unreachable, so live sends cannot be accepted. Nothing was queued. Retrying is safe. Back off exponentially.
service_unavailable503RetryBlueticked could not verify your workspace state. The request was not applied. Retry shortly with backoff.
provider_error502A personAn upstream provider refused or failed — Paystack declining a payment link, for example. Unlike service_unavailable this usually will not fix itself. Check the request and the provider account connected in the workspace. A bare retry will fail the same way.
insufficient_balance402A personThe prepaid ZAR wallet does not have enough available credit for this send. Billing is prepaid, so this is the failure a long-running integration is most likely to meet. A person must top up. error.details carries estimated_cost_zar_cents, balance_zar_cents and available_zar_cents, so you can report exactly how short the wallet is. Sandbox sends are never billed.
spend_cap_reached402A personThis API key has reached the monthly spend cap set on it. The workspace wallet is untouched — the ceiling belongs to the credential. Do not retry: the stop is deliberate and only a person can lift it, by raising or removing the cap in Developers → API keys, or by waiting for the month to roll.
plan_limit_reached403A personA per-tier plan cap is exhausted — seats, published sites, contacts or WhatsApp numbers. error.details carries { key, tier, limit, current } naming which. A person must upgrade the plan or free capacity.
subscription_inactive403A personThe workspace subscription is suspended or cancelled, so the gated action is refused. Nothing will succeed until a person restores the subscription.
internal_error500RetryBlueticked accepted the request but hit an internal failure. Retry with backoff. If it persists, quote request_id to support — it identifies this exact request in our logs.
variables_mismatch400Your codeThe supplied template variables do not match the template's {{n}} markers — a missing or an extra position. Read the template with GET /api/v1/templates/{id} to see the positions it declares, then send exactly those.
button_params_mismatch400Your codeThe 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. Read the template with GET /api/v1/templates/{id} for its button definitions.
document_invalid400Your codeThe document parameter is malformed: url must be https and filename at most 240 characters. The URL has to be fetchable by WhatsApp once, at send time — a short-lived pre-signed URL is the right shape.
document_required400Your codeThe template has a DOCUMENT header, so a document is required on the send. Pass document: { url, filename }.
document_not_applicable400Your codeA document was supplied but the template has no DOCUMENT header, or the send is not a template send. Send it without one, or use a template that declares the header.
document_source_invalid400Your codeThe campaign document_source is malformed. Check mode, field_key, filename_template and missing_policy against the campaigns reference.
document_source_required400Your codeThe campaign's template has a DOCUMENT header, so it needs a document_source to resolve one per recipient. Add a document_source to the campaign.
document_source_not_applicable400Your codeA document_source was supplied but the campaign is neither an email campaign nor a WhatsApp template with a DOCUMENT header. Remove document_source.
template_kind_unsupported422Your codeThe WhatsApp template is a kind Blueticked can't send yet: details.kind is product (Product or Item list buttons, a product header or product cards), carousel, expiring_offer (a limited-time offer with an expiry) or coupon_code. No send path supplies those per-send values, so nothing was stored or sent. Send a template without those parts. Retrying the same template will not help.

Rate limits

A 429 rate_limitedcan come from either of two buckets — the key's own, or the workspace ceiling every key shares — and error.details.scope says which. See Rate limits and retries for the ceilings and the headers to pace against.