BluetickedDeveloper docs

Errors

Every machine-readable error code the API can return, and what to do about each.

The shape of a failure

Every error returns the same envelope, whatever went wrong.

Error response
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "\"loyalty_tier\" is not a defined custom field. Valid keys: none defined.",
    "details": {
      "field": "loyalty_tier",
      "code": "unknown_custom_field"
    },
    "next_steps": [
      {
        "action": "correct_request",
        "by": "caller",
        "description": "The message names the offending field. Fix it and retry; the same body will fail again."
      }
    ]
  },
  "request_id": "req_ca7d43c17cab7ea1c60aa97a8c2668ca"
}

Branch on code, never on message: messages are prose and may be reworded. The request_id is also on the x-request-id header, and it identifies the exact call in your request log; quote it in support requests.

next_steps is always present and never empty. Each step carries a stable action to branch on, a sentence for your logs, and (the part worth writing code against) by, which says who can clear the failure: caller means you can fix it yourself and retry, human means a person has to act in a browser (surface it and stop retrying: no rewrite of the request will help), and waiting means something outside anyone's control, such as a Meta review, so poll. Steps that are fixed by a call carry endpoint; steps a person must clear carry url.

Two kinds of validation_failed

Both answer 400 validation_failed, and details tells them apart.

When the body does not fit the schema, details is the list of schema issues, each with the field's path and a message. When the body fits but a value breaks a rule, details names the field.

The body does not fit the schema
POST /api/v1/messages
{
  "channel": "whatsapp",
  "body": "Hello"
}

HTTP 400
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "Invalid message: `to` — Invalid input: expected object, received undefined",
    "details": [
      {
        "expected": "object",
        "code": "invalid_type",
        "path": [
          "to"
        ],
        "message": "Invalid input: expected object, received undefined"
      }
    ],
    "next_steps": [
      {
        "action": "correct_request",
        "by": "caller",
        "description": "The message names the offending field. Fix it and retry; the same body will fail again."
      }
    ]
  },
  "request_id": "req_3a0a501cdcc36329614c739fe760383f"
}
A value breaks a rule
POST /api/v1/contacts
{
  "phone": "+27800000001",
  "custom_fields": {
    "loyalty_tier": "gold"
  }
}

HTTP 400
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "\"loyalty_tier\" is not a defined custom field. Valid keys: none defined.",
    "details": {
      "field": "loyalty_tier",
      "code": "unknown_custom_field"
    },
    "next_steps": [
      {
        "action": "correct_request",
        "by": "caller",
        "description": "The message names the offending field. Fix it and retry; the same body will fail again."
      }
    ]
  },
  "request_id": "req_ca7d43c17cab7ea1c60aa97a8c2668ca"
}

Rehearse a failure

Error handling is the one part of an integration you cannot reach by accident in the sandbox.

With a test key, ask for a failure by its code and get the real response: the real status, code and next_steps.

The x-simulate-error header
curl "https://app.blueticked.com/api/v1/channels" \
  -H "Authorization: Bearer $BLUETICKED_API_KEY" \
  -H "X-Simulate-Error: channel_unavailable"

Test keys only: a live key that sends the header is refused, so a test suite cannot pass against production while proving nothing. The header is read after authentication and rate limiting, so it uses quota like any other call. The codes you can ask for:

channel_unavailable, insufficient_balance, spend_cap_reached, rate_limited, plan_limit_reached, subscription_inactive, queue_unavailable, service_unavailable, provider_error, validation_failed, idempotency_conflict, resource_in_use, not_found, forbidden

Details some codes carry

error.details adds what a code needs to be acted on.

Two you will meet early:

rate_limited
details.scope says which bucket refused the call: "key" (this key's own allowance) or "organization" (the plan allowance every key shares), so you know whether to slow one worker or the whole fleet. details.limit is that bucket's ceiling and details.reset_at (ISO 8601) when it refills.
insufficient_balance
details.estimated_cost_zar_cents is what the send would cost, details.balance_zar_cents the purchased top-up balance, and details.available_zar_cents what the send can draw on: the plan's included credit plus the top-up (plus a small grace for a transactional send). All in ZAR cents.

Every code

34 stable, machine-readable codes.

This list is the same one the OpenAPI Error enum is generated from, so it cannot fall behind the API.

invalid_field_mapHTTP 400The inbound field map has an unknown or ambiguous target or repeats a contact identity target.Your code clears it: Use an unambiguous custom field key and map each contact identity target from only one source.
unauthorizedHTTP 401The credential was refused: a missing or malformed Authorization header, an unknown key or a wrong secret, a revoked or expired key, an expired or revoked OAuth token, or an inactive workspace. Once a key's secret checks out, details.reason says which (revoked, expired, workspace_inactive).Your code clears it: Send a live or sandbox key as `Authorization: Bearer`. If the key was revoked or rotated, mint a new one.
forbiddenHTTP 403The credential is valid but not allowed here: a missing scope (details.required), an address outside the key's IP allowlist, a business-restricted key on an endpoint it cannot use, or a blu_pub_ key off the agent chat endpoint or its allowed origins.A person clears it: The key is missing a scope this endpoint requires. `error.details.required` lists them; an owner or admin can edit this key's scopes under Developers > Keys > Manage with two-step verification. Agent and widget keys keep their server-managed scopes. Do not retry unchanged.
not_foundHTTP 404The requested contact, template, message or campaign does not exist.Your code clears it: The id does not exist in this workspace. List the resource to find a valid id rather than retrying — an id from another workspace also reports not found.
validation_failedHTTP 400The request was refused before anything happened. Either the body does not fit the schema (details is the list of schema issues, each with the field's path), or a value breaks a rule (details names the field).Your code clears it: The message names the offending field. Fix it and retry; the same body will fail again.
rate_limitedHTTP 429The workspace or key exceeded a rate limit, or the calling address made too many failed authentication attempts. Wait Retry-After seconds, then retry.Your code clears it: Wait the number of seconds in `retry-after`, then retry; `x-ratelimit-reset` is the same moment as an ISO-8601 timestamp. `x-ratelimit-limit` and `x-ratelimit-remaining` on every response let you pace before you hit this at all.
idempotency_conflictHTTP 409The same Idempotency-Key was reused with a different body.Your code clears it: This key was already used with a different body. Generate a fresh UUID per logical operation — never reuse one across different payloads.
idempotency_in_progressHTTP 409A request with the same Idempotency-Key is still being processed; retry once it finishes.Your code clears it: An identical request is still running. Wait briefly and retry with the SAME key to collect its result.
resource_in_useHTTP 409The 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.Your code clears it: Something still depends on this resource. `error.details` lists what; remove or repoint those first.
conflictHTTP 409The 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, as when 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, so send a different value or reconcile the state and retry.Your code clears it: The request collides with what is already there: that code or name is taken, or the resource is not in a state this call accepts. `message` says which. Send a different value, or bring the resource into the required state first — the same body will keep failing, and nobody has to open the dashboard for you.
method_not_allowedHTTP 405The 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.)Your code clears it: Check the endpoint's method in the OpenAPI document.
goneHTTP 410The endpoint was retired. It keeps answering 410, for at least one SDK major, with `details.successor` naming what replaced it; the same request will never succeed again, so move to the successor rather than retrying.Your code clears it: This endpoint was retired and will not come back. `details.successor` names the endpoint that replaced it and `message` says how to move; the same request keeps answering 410.
channel_unavailableHTTP 503The requested provider channel is disconnected or not ready.A person clears it: No channel is connected for this workspace. Mint a connect link and send it to the account owner — they complete Meta's flow themselves. Check GET /api/v1/readiness for the full picture first.
queue_unavailableHTTP 503The send queue is not configured, so live sends cannot be accepted.It clears with time: The send queue is unreachable. Nothing was queued, so retrying is safe. Back off exponentially.
insufficient_balanceHTTP 402The prepaid wallet does not have enough available credit for the requested send.A person clears it: The prepaid ZAR wallet cannot cover this send. A person must top up. Sandbox sends are never billed.
spend_cap_reachedHTTP 402This 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.A person clears it: This key's own monthly ceiling is spent — the workspace wallet is untouched. Deliberate stop, not a transient error: do not retry. A person raises the cap or waits for the month to roll.
plan_limit_reachedHTTP 403A per-tier plan cap (seats, sites, contacts or WhatsApp numbers) is exhausted. `details` carries { key, tier, limit, current }. Maps to 403.A person clears it: A per-tier limit is exhausted. `error.details` names which. A person must upgrade or free capacity.
subscription_inactiveHTTP 403The organization's subscription is suspended or cancelled, so the entitlement-gated action is refused. Maps to 403.A person clears it: The workspace subscription is suspended or cancelled. Nothing will succeed until a person restores it.
variables_mismatchHTTP 400The supplied template variables do not match the template's {{n}} markers (missing or extra positions).Your code clears it: Supplied variables do not match the template's {{n}} markers. Read the template to see the positions it declares, then send exactly those.
button_params_mismatchHTTP 400The 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.Your code clears it: template_buttons does not match the template's dynamic URL buttons. Read the template for its button definitions.
document_invalidHTTP 400The document parameter is malformed: url must be https and filename at most 240 characters.Your code clears it: The document object is malformed. `url` must be https and reachable by WhatsApp at send time; `filename` is capped at 240 characters.
document_requiredHTTP 400The template has a DOCUMENT header; pass document: { url, filename }.Your code clears it: This template has a DOCUMENT header, so a `document` is required.
document_not_applicableHTTP 400A document was supplied but the template has no DOCUMENT header (or the send is not a template send).Your code clears it: This template has no DOCUMENT header, so it rejects a `document`. Send it without one, or use a template that declares the header.
document_source_invalidHTTP 400The campaign document_source is malformed; check mode, field_key, filename_template and missing_policy.Your code clears it: document_source is malformed. Check `mode`, `field_key` and `missing_policy` against the campaigns reference.
document_source_requiredHTTP 400The campaign template has a DOCUMENT header; pass a document_source.Your code clears it: This campaign's template has a DOCUMENT header, so it needs a document_source to resolve one per recipient.
document_source_not_applicableHTTP 400A document_source was supplied but the campaign is neither an email campaign nor a WhatsApp template with a DOCUMENT header.Your code clears it: This campaign's template has no DOCUMENT header, so document_source does not apply.
template_kind_unsupportedHTTP 422The template is a kind Blueticked can't send yet: Product or Item list buttons, a product header or cards (product), carousel cards (carousel), a limited-time offer with an expiry (expiring_offer) or a coupon-code button (coupon_code). details.kind names it. No send path supplies these per-send values, so choose a template without them. POST /api/v1/templates answers it too, with details.field, for a header field in the body (any field whose name starts with header, in any case: header, header_format, headerFormat): the API creates templates without a header, so make one with a header in the dashboard.Your code clears it: Blueticked can't send this kind of template yet (details.kind is product, carousel, expiring_offer or coupon_code). Send a template without it.
internal_errorHTTP 500Blueticked accepted the request but hit an internal failure.It clears with time: Unexpected failure on our side. Retry with backoff; if it persists, quote `request_id` to support — it identifies this exact request in our logs.
payload_too_largeHTTP 413The request body is above the endpoint's size limit; returned as 413 before the body is validated.Your code clears it: Split the request into smaller batches. This is checked before the body is parsed, so nothing was applied.
service_unavailableHTTP 503Blueticked could not verify your workspace state; the request was not applied, so retry shortly.It clears with time: A dependency we own is unavailable and the request was not applied. Retry shortly.
provider_errorHTTP 502An 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.A person clears it: An upstream provider refused — Paystack declining a payment link, for example. Usually the request or the connected provider account needs changing, so a bare retry will fail the same way.
enable_requires_dashboardHTTP 400The API can only pause an AI agent. Re-enabling one is a dashboard action, where its persona and escalation rules are visible.A person clears it: The 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; do not retry.
activation_requires_dashboardHTTP 400The API can only pause a flow. Activating one is a dashboard action, so nobody ships a live flow sight-unseen.A person clears it: Flows are created paused by design. A person activates them in the dashboard. Surface this; do not retry.
journey_not_activeHTTP 409Only an active journey accepts enrolments; a draft, paused or archived one is refused. Activation is a dashboard action.A person clears it: Only an active journey accepts enrolments. A person must activate it before this call can succeed.

Reasons

Some errors carry details.reason, naming which failure of that kind it was, so a program can branch on it without reading the message.

96 reasons today; a value not listed here means the code's general case.

rotation_grace_activeconflictAn earlier secret is still in its rotation grace window. Wait until details.expires_at, or explicitly choose end_old_secret=now to end every earlier secret. PATCH /api/api-keys/{id} (dashboard)
key_revokedconflictThe key was revoked before or during this edit. Create a new key to continue. PATCH /api/api-keys/{id} (dashboard)
list_not_foundnot_foundThe audience list is missing, deleted, or outside this organisation and business. Choose an available list. POST /api/v1/campaigns
segment_not_foundnot_foundThe audience segment is missing, deleted, or outside this organisation and business. Choose an available segment. POST /api/v1/campaigns
setup_recheck_too_soonrate_limitedMeta was asked about this number less than a minute ago. Wait retryAfter seconds, then check again. POST /api/setup/recheck (dashboard)
audience_uncheckedservice_unavailableThe audience source could not be checked. Nothing was saved; retry when the service is available. POST /api/v1/campaigns
erasure_retryingservice_unavailableWork on the contact is still in progress (an Ask Blu answer about them, or a large backlog). Nothing was stamped; send the same request again after the Retry-After seconds, and contact support if it keeps answering 503. POST /api/v1/contacts/{id}/erase
revokedunauthorizedThe key was revoked. It will not work again: create a new key in Developers → Keys. Every /api/v1 call
expiredunauthorizedThe key's expiry date has passed. Rotate it to make it work again: a rotate clears an expiry that has passed. Every /api/v1 call
workspace_inactiveunauthorizedThe key is valid but its workspace is no longer active, so no key in it can call the API. Every /api/v1 call
no_draftvalidation_failedThe site has no draft to publish. Edit the site first. POST /api/v1/sites/{id}/publish
invalid_draftvalidation_failedThe site's draft does not pass the site schema; error.message names the problem. Fix the draft, then publish. POST /api/v1/sites/{id}/publish
live_address_unavailableservice_unavailableThe site's live address could not be prepared (or is already in use elsewhere); nothing was published. Try again, or contact support if the message says the address is in use. POST /api/v1/sites/{id}/publish
publish_failedinternal_errorBlueticked could not publish the site. Try again. POST /api/v1/sites/{id}/publish
not_publishedvalidation_failedThe site is not published, so there is nothing to take offline. POST /api/v1/sites/{id}/unpublish
unpublish_failedinternal_errorBlueticked could not take the site offline. Try again. POST /api/v1/sites/{id}/unpublish
business_not_foundnot_foundNo business this key can reach matches business_id. List the workspace's businesses with GET /api/v1/me. POST /api/v1/sites
business_limit_reachedplan_limit_reachedThe workspace holds as many businesses as its plan includes; details.limit says how many and error.message names the next plan. Move to a larger plan in Billing, or remove a business first. POST /api/v1/businesses
business_charge_requiredplan_limit_reachedAdding this business costs money on this plan, and nothing was created. details.billing gives unit_zar_cents (a month), part_zar_cents (the rest of this period) and charged_now. Send the request again with accept_business_charge: { monthly_zar_cents: <unit_zar_cents> } to add it. POST /api/v1/businesses
business_charge_changedplan_limit_reachedThe monthly price you accepted is no longer the price of another business (the plan or your number of businesses changed), and nothing was created. details.billing gives today's price; accept that one to add it. POST /api/v1/businesses
business_charge_live_key_requiredplan_limit_reachedAdding this business costs money on this plan, and a blu_test_ key cannot start a real charge; nothing was created. details.billing gives the price. Use a live key to add it. POST /api/v1/businesses
operator_agreement_unsignedforbiddenThis workspace was made after the operator agreement came into force, and its owner has not signed it yet, so nothing was sent. The owner signs it in Settings, Legal (next_steps carries the address); then send again. A blu_test_ key is not asked. POST /api/v1/messages, POST /api/v1/campaigns, POST /api/v1/events/send, POST /api/v1/journeys/{id}/enroll
no_organizationforbiddenThe signed-in user belongs to no workspace (dashboard session only). POST /api/v1/sites
unknown_templatevalidation_failedNo site template has that name; leave template out for the blank starter. POST /api/v1/sites
slug_unavailableconflictNo free site address could be found from desired_slug; error.message says why. Choose another desired_slug. POST /api/v1/sites
insert_failedinternal_errorBlueticked could not create the site, or could not check whether its address is free. Nothing was created; try again. POST /api/v1/sites
recipient_not_foundnot_foundNo contact matches `to`. Pass `to.create: true` to create it in the send, or create it with POST /api/v1/contacts first. POST /api/v1/messages
custom_identity_requiredvalidation_failedA marketing email must come from your own verified sending domain, not the shared Blueticked sender. Verify a domain in Email → Settings, or send it as transactional if it is one. A blu_test_ key does not check this. POST /api/v1/messages
dmarc_requiredvalidation_failedA marketing email needs a confirmed DMARC record on the sending domain. Publish the record shown in Email → Settings, run Check now, then send again. POST /api/v1/messages
business_identity_requiredvalidation_failedA marketing email carries the business's registered legal name in its footer, and the business profile has none. Add it, then send again. POST /api/v1/messages
physical_address_requiredvalidation_failedA marketing email carries a complete postal address in its footer. Add the street, city, postal code and country to the business profile, then send again. POST /api/v1/messages
freemail_reply_tovalidation_failedThe reply-to is a Gmail, Outlook, Yahoo, iCloud or other freemail address, on the request's reply_to or the sender's configured Reply-To. Use an address on the business's own domain. POST /api/v1/messages
reply_to_not_alignedvalidation_failedThe reply-to is not on the sending domain: a request's reply_to must use the active sender's own domain, and a configured Reply-To must match it. Change the reply-to, or leave reply_to out. POST /api/v1/messages
contacts_write_requiredforbidden`to.create: true` creates a contact, which needs the contacts:write scope on the key as well as the send scope. POST /api/v1/messages
domain_in_use_elsewhereconflictAnother Blueticked workspace sends from this domain. Use a domain you control that no other workspace uses. POST /api/v1/email/sending-domain
domain_in_use_by_businessconflictAnother business in this workspace sends from this domain. Use another domain, or remove it from that business first. POST /api/v1/email/sending-domain
domain_registered_unclaimedconflictThe email provider already has this domain but no workspace holds it. Contact Blueticked support with request_id to release it; a retry gets the same answer. POST /api/v1/email/sending-domain
domain_holder_uncheckedservice_unavailableBlueticked could not read who holds the domain, so nothing was added. Retry the same request in a moment. POST /api/v1/email/sending-domain
receive_onlyconflictThe integration receives events from an app, so it has nothing to send and no outbound mapping to set or test. PATCH /api/v1/integrations/{id}, POST /api/v1/integrations/{id}/test
website_form_channelconflictA website form sends WhatsApp and SMS only to a visitor who ticks its consent box, so its `inbound_target_channel` cannot be `whatsapp` or `sms`. Write a sentence for it instead; `email` can be bound. PATCH /api/v1/integrations/{id}
invalid_contact_idsvalidation_failed`contact_ids` must be a non-empty array of contact ids, within the per-call limit. POST /api/v1/journeys/{id}/enroll
business_id_requiredvalidation_failedThe workspace has more than one business, so the flow must name one (see GET /api/v1/me). POST /api/v1/studio-flows
automations_retiredgoneThe classic automations API was retired on 21 October 2026. Use Flow Studio flows (details.successor, /api/v1/studio-flows) and start them with events (POST /api/v1/events/send). GET, POST /api/v1/automations; GET, PATCH, DELETE /api/v1/automations/{id}; GET /api/v1/automations/recipes
definition_requiredvalidation_failedPass `definition`: the whole Flow Studio flow, as GET /api/v1/studio-flows/{id} returns it. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_business_idvalidation_failed`business_id` must be the id of a business (see GET /api/v1/me). POST /api/v1/studio-flows
invalid_definitionvalidation_failedThe definition is not a Flow Studio flow; `details.detail` says what is missing. Nothing was saved. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
compile_failedvalidation_failedThe flow would not work as written; `details.issues` lists each problem (`path`, `message`), the checklist Flow Studio shows. Nothing was saved. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
sms_type_requiredvalidation_failedAn SMS message the flow sends itself (before its first wait) has no `smsMessageType`. Set `transactional` for an order or service update, or `marketing` for anything promotional (it needs SMS consent and keeps quiet hours); `details.issues` names each step. Nothing was saved. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
edit_version_requiredvalidation_failedPass `expected_updated_at`: the `updated_at` of the flow you read. PATCH /api/v1/studio-flows/{id}
conflictconflictSomeone saved the flow since it was read. Read it again, then send the change; nothing was overwritten. PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
studio_flow_liveresource_in_useThe flow is on, so the API does not change it. Switch it off first (POST /api/v1/studio-flows/{id}/switch-off), or edit it in Flow Studio. PATCH /api/v1/studio-flows/{id}
studio_flow_shared_entryresource_in_useThe flow starts a journey it shares with other flows, a link that is set in Flow Studio, so it is edited there. PATCH /api/v1/studio-flows/{id}
journey_read_failedservice_unavailableThe journeys the flow starts could not be checked just now. Nothing was saved; try again. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
sequence_requires_send_stepvalidation_failedA journey in the flow has no message to send. Add a message step. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_triggervalidation_failedThe flow's start is not one a journey can begin with. Choose another start in the definition. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
flow_name_takenconflictAnother flow in this business already has that name. Change `name` in the definition and send it again. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_stepsvalidation_failedA journey in the flow has a step that cannot be saved; `message` says which. Nothing was saved. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_segmentvalidation_failedA step names a segment that does not exist in this workspace. Choose one from the workspace's segments. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_email_templatevalidation_failedA step names an email template that does not exist in this workspace. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
invalid_whatsapp_templatevalidation_failedA step names a WhatsApp template that does not exist in this workspace. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
unsupported_whatsapp_templatevalidation_failedA step names a WhatsApp template a journey cannot send (its buttons or media need something the step cannot give). POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
whatsapp_template_not_approvedvalidation_failedA step names a WhatsApp template Meta has not approved yet. Wait for approval, or choose another. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
whatsapp_template_not_attachedvalidation_failedA step names a WhatsApp template that is not on this business's WhatsApp number. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
not_foundnot_foundThe flow does not exist for this key: it was deleted, or it is in a business the key cannot reach. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
forbiddenforbiddenThe flow is in a business this caller cannot change. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
reference_outside_businessvalidation_failedThe flow starts a journey, or begins on a segment, that belongs to a business this key cannot reach; `details.issues` names each step. Nothing was saved. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}
meta_refusedprovider_errorMeta did not accept the template; the message carries Meta's own reason. POST /api/v1/templates/{id}/submit
journeys_list_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. GET /api/v1/journeys
ai_agents_list_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. GET /api/v1/ai-agents
form_submissions_list_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. GET /api/v1/form-submissions
studio_flow_save_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
automation_save_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
save_failedinternal_errorBlueticked could not complete the call on its side. Nothing changed; retry with backoff. POST /api/v1/studio-flows, PATCH /api/v1/studio-flows/{id}, POST /api/v1/studio-flows/{id}/switch-off
table_not_foundnot_found`audience.table_id` is not a table of this business (or of the whole organisation). Nothing was saved. POST /api/v1/campaigns
table_has_no_peoplevalidation_failed`audience.table_id` is a reference table: its rows belong to no one, so it can't be a campaign's audience. POST /api/v1/campaigns
table_status_unknownvalidation_failed`audience.status` is not one of the table's status keys. Name one of them, or leave it out for every row. POST /api/v1/campaigns
table_column_unknownvalidation_failedA `row_field` template value names a column the table does not have. Map it to one of the table's column keys. POST /api/v1/campaigns
row_field_needs_tablevalidation_failedA template value has `source: "row_field"`, which reads a table row, but the campaign's audience is not a table. POST /api/v1/campaigns
table_uncheckedservice_unavailableBlueticked could not read the table to check it. Nothing was saved; retry with backoff. POST /api/v1/campaigns
assignee_not_membervalidation_failedassigned_to is not a current member of this organisation. Nothing was changed; choose a workspace user. PATCH /api/v1/conversations/{id}
assignee_cannot_openvalidation_failedassigned_to cannot open this conversation: they need access to its business, or, when it has no business, to be an owner or admin. Nothing was changed. PATCH /api/v1/conversations/{id}
media_pendingconflictThe file is still being fetched from the channel (media_status is pending). Retry in a few seconds. GET /api/v1/conversations/{id}/messages/{messageId}/media
media_unavailablenot_foundThe file cannot be served: its download from the channel failed for good (media_status is failed), or it is no longer stored. GET /api/v1/conversations/{id}/messages/{messageId}/media
invalid_formvalidation_failedThe body is not multipart/form-data. Send the PDF as a `file` part with `contact_id`; JSON and base64 bodies are not accepted. POST /api/v1/documents
missing_filevalidation_failedThe form has no `file` part. Attach the PDF as `file`. POST /api/v1/documents
invalid_contact_idvalidation_failed`contact_id` is missing or is not a UUID. Pass the id of the contact the document belongs to. POST /api/v1/documents
file_too_largevalidation_failedThe PDF is larger than 4 MB, the most an API upload takes (the request body is capped at 4.5 MB). Nothing was stored. POST /api/v1/documents
not_a_pdfvalidation_failedThe bytes are not a complete PDF: a PDF header and an end-of-file marker are both required, whatever the Content-Type says. Nothing was stored. POST /api/v1/documents
document_not_foundnot_found`document.id` names no document in this organisation, or the document was deleted. Upload it with POST /api/v1/documents first. POST /api/v1/messages
document_not_for_contactvalidation_failedThe document belongs to another contact, or the send would create its recipient. A document is only sent to the contact it was uploaded for; nothing was written. POST /api/v1/messages
not_whatsapp_channelvalidation_failedThe channel is not a WhatsApp channel; `details.channel` says what it is. Spend is Meta's WhatsApp billing, so only a WhatsApp channel has one. GET /api/v1/channels/{id}/whatsapp-spend
whatsapp_token_missingchannel_unavailableThe WhatsApp channel has no usable Meta connection to read spend with, or Meta refused its token (expired or revoked). Reconnect it in the dashboard; retrying will not help until then. GET /api/v1/channels/{id}/whatsapp-spend
meta_unavailableprovider_errorMeta answered with an error instead of the spend (`details.meta_status` is Meta's status). Retry shortly; it is never reported as an empty spend. GET /api/v1/channels/{id}/whatsapp-spend
meta_unavailableservice_unavailableMeta could not be reached at all. Retry shortly; it is never reported as an empty spend. GET /api/v1/channels/{id}/whatsapp-spend
meta_pricing_permission_requiredprovider_errorMeta has not enabled pricing analytics for this WhatsApp Business Account yet. Retrying will not help; it opens on Meta's side. GET /api/v1/channels/{id}/whatsapp-spend

Two answers forward a reason from elsewhere, so their values are open-ended:

  • POST /api/v1/templates/{id}/submit (internal_error): The submit pipeline's own snake_case reason (for example meta_unreachable when Meta could not be reached).
  • POST /api/v1/messages (queue_unavailable): The send queue's own error text, when a message could not be handed to the queue. Nothing was sent.