Platform

Errors

Every non-2xx response uses the same JSON envelope so your integration only parses one error shape.

Envelope

{
  "error": {
    "type":    "validation_error",
    "code":    "invalid_message_request",
    "message": "to is required.",
    "param":   "to",
    "request_id": "req_...",
    "trace_id":   "trc_..."
  }
}

type is the category (stable). code is the specific reason (also stable; treat it as a string). message is human-readable and may change. param is set on validation errors. Rate-limit errors also include retry_after_ms. trace_id is the single field to include in a support ticket.

Reporting an unexpected failure

When a public /v1/* call fails with an unhandled internal_error — something genuinely unexpected on our side, not a typed operational error — the envelope carries an additive, optional feedback pointer telling an integrating agent where to report it:

{
  "error": {
    "type":    "internal_error",
    "code":    "internal_error",
    "message": "An internal error occurred.",
    "request_id": "req_...",
    "trace_id":   "trc_...",
    "feedback": { "endpoint": "/v1/feedback", "method": "POST" }
  }
}

Post the trace_id (and, if you have it, the request_id and the code) to POST /v1/feedback as the related_error so the report correlates to the failure. The endpoint is a constant relative path — always /v1/feedback, never derived from the request host.

The pointer is deliberately narrow. It appears only on an unhandled internal_error from a public /v1/* route that is not /v1/feedback itself. It is absent from every 4xx, from typed operational 5xx such as provider_error, from rate-limit and idempotency responses, and from all dashboard errors. The field is additive, so an older SDK that does not know about it simply ignores it. See the /api-reference/feedback page for the request shape and the receipt.

Categories

StatusTypeWhen
400validation_errorYour request doesn't match the contract. Fix and retry.
401authentication_errorMissing, malformed, or revoked API key. The signature or HMAC failed.
402payment_requiredCredit balance exhausted or project spend cap hit. Top up or raise the cap.
403authorization_errorThe key is valid but the required scope is missing.
404not_foundResource doesn't exist — or exists in another organization. Always 404, never 403.
409conflict / idempotency_conflictState transition not allowed, or idempotency-key replay with a different body.
429rate_limitedAPI-key or phone-number throughput bucket exhausted. Honor Retry-After and error.retry_after_ms.
500internal_errorSomething went wrong on our side. Safe to retry idempotent calls.
502provider_errorMeta, Salvy, or a platform-owned generation provider returned an error. The message carries the normalized provider detail.
503internal_errorWebhook receiver misconfigured (missing app secret). Ops issue.

Common codes worth handling explicitly

CodeMeaning
credit_balance_exhaustedOrg balance ≤ -5 BRL. Top up and retry.
account_suspendedThe organization is suspended (trust-and-safety / abuse enforcement). Returns 403 at every auth boundary. Contact support to restore access — retries will not clear it.
spend_limit_exceededProject monthly cap reached. Next period or raise the cap.
kyc_requiredProduction Salvy phone provisioning requires an approved business identity and none is on file (never submitted, or rejected). Submit it in the dashboard (Settings → Business identity). Returns 403. Sandbox is never KYC-gated.
kyc_pendingBusiness identity submitted but not approved yet (review in progress or resubmission requested). The operation unlocks on approval. Returns 403.
contact_opted_outRecipient has opted out — send blocked. Do not retry without an explicit override.
invalid_phone_country_calling_codeA structured WhatsApp recipient has an unknown country calling code. Correct the field before retrying.
invalid_phone_national_numberA structured WhatsApp recipient has an invalid national number for its calling code.
invalid_phone_e164The structured calling code and national number do not form a valid E.164 phone. Legacy recipient.id acceptance is unchanged.
idempotency_key_conflictSame idempotency-key, different body hash. Use a fresh key.
invalid_flow_jsonFlow JSON failed structural validation before submission.
automation_inbound_number_conflictAnother enabled inbound auto-reply automation is already bound to this phone_number_id. Only one per number per environment. Returns 409.
phone_number_confirmation_mismatchPOST /v1/phone-numbers/{id}/transfer body field confirm_phone_number_id does not match the route id. Returns 400.
phone_number_transfer_same_environmentTransfer target environment equals the source environment. Pick a different target. Returns 400.
phone_number_not_transferableThe number is in a status that cannot be transferred (for example mid-provisioning or terminal). Returns 409.
phone_number_limit_reachedThe organization already holds the maximum number of concurrently active Salvy-provisioned numbers (production only; deployment default 2). Release an existing number to free its slot, or contact support to raise the limit. Returns 409.
phone_number_not_removableDashboard remove of a phone number that is not yet terminal. Only failed, disconnected, or released numbers can be removed — disconnect or release first. Returns 409.
template_not_approvedTemplate send or cost estimate references a draft, rejected, paused, disabled, or missing approved template. Submit and wait for approval first.
template_param_mismatchAn outbound template send was rejected by Meta because the supplied parameters do not match the approved template (count or format). The raw Meta detail is on the failed message’s provider_error (see GET /v1/messages/:id).
provider_connection_unauthorizedMeta rejected the environment’s stored connection credential (HTTP 401 / OAuthException 190) — the token expired or was revoked, and the connection is suspended. Every send fails with this code until an owner/admin re-authenticates the Meta connection with a permanent System User token (dashboard Connections page or POST /v1/provider-connections/:id/rotate).
sandbox_payment_status_sandbox_onlyThe sandbox payment status fixture was called with a production key. Use only sandbox environments.
sandbox_payment_status_terminalA sandbox payment is already terminal and cannot be changed to a different terminal status.
payment_provider_options_unsupportedProvider-specific payment options do not match the selected payment provider connection — send options only for the provider the environment resolves to. The connectionless sandbox default fixture does not emulate Abacate Pay checkout; select an Abacate Pay sandbox connection before sending provider_options.abacate_pay.
payment_provider_disabledThe payment provider the environment resolves to is not currently offered by the platform. Configure an Abacate Pay payment connection instead.
template_generation_not_configuredPlatform-owned template generation credentials are missing.
template_generation_provider_errorTemplate draft provider call failed. Safe to retry with the same idempotency key.
llm_route_not_foundNo LLM route is configured for this environment.
llm_cost_cap_reachedDaily BRL cap on the LLM route hit. Raises 402.

Retry guidance