Errors
Errors respond with the appropriate HTTP status code and a JSON body. There are two forms:
Business errors (what you asked for can't be done): they carry a human-readable message and, when your code needs to tell them apart, a stable code.
{ "statusCode": 409, "code": "CONTACT_PHONE_EXISTS", "message": "Ya existe un contacto con ese teléfono.", "contactId": "0b1c2d3e-…", "fullName": "Laura Pérez" }Validation errors (the body or the parameters don't meet the contract): statusCode 400 and a list of messages, one per field. An unknown field or parameter is also rejected (property x should not exist): that way a typo doesn't silently pass as "no filter".
{ "statusCode": 400, "message": ["primaryPhone must be shorter than or equal to 32 characters"], "error": "Bad Request" }HTTP status codes
| Code | When |
|---|---|
| 400 | Validation, or an action that doesn't apply in the current state (campaign already sent, no recipients, insufficient balance, date in the past) |
| 401 | Invalid, revoked or expired key |
| 403 | Missing scope, or route not open to keys |
| 404 | The resource doesn't exist or doesn't belong to your organization |
| 409 | Conflict: phone already used by another contact, channel identity already linked, contact with history |
| 429 | Rate limit |
| 5xx | Our error: retry with exponential backoff and, if it persists, contact us |
Stable error codes
code |
HTTP | Meaning |
|---|---|---|
API_KEY_INVALID |
401 | The key doesn't exist, is mistyped or was revoked |
API_KEY_EXPIRED |
401 | The key has expired: create another one |
API_KEY_SCOPE_MISSING |
403 | The route requires a scope the key doesn't have; the message says which one |
API_KEY_NOT_ALLOWED_HERE |
403 | The route is not open to API keys |
API_KEY_RATE_LIMITED |
429 | More than 600 requests in one minute with that key |
CONTACT_PHONE_EXISTS |
409 | A contact with that phone already exists; the response includes contactId and fullName |
CONTACT_HAS_HISTORY |
409 | A contact with conversations, campaigns or deals can't be deleted |
CAMPAIGN_VARIABLES_REQUIRED |
400 | The template has variables and the campaign doesn't say where they come from |
PHONE_NUMBER_NOT_OWNED |
409 | The sender number doesn't belong to your organization |
The remaining business errors come with message only; the most frequent ones are listed in the reference for each route. Treat code values as stable: they won't change within the version. message texts may improve over time and are meant to be displayed, not compared.
Insufficient balance
Sending a campaign charges the batch to the wallet before calling Meta. If the balance isn't enough, POST /campaigns/{id}/send responds 400 with "Saldo insuficiente en la wallet." (insufficient wallet balance) and the campaign stays as it was; if the balance runs out in the middle of a staggered send, the campaign is paused with that reason in stopReason and is resumed with POST /campaigns/{id}/resume after topping up. Check GET /campaigns/{id}/preview-cost before sending: wallet.sufficient tells you without attempting the send.