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 33codes 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.
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.