# Error Handling

All eCommunicate API errors follow a consistent JSON format with standard HTTP status codes. This guide covers the error response structure, common error scenarios, and delivery failure codes.

## Error response format

Every error response contains four fields:

```json
{
  "status": "ERROR",
  "message": "Description of what went wrong",
  "requestId": "c3a1f8e2-4b6d-11ee-be56-0242ac120002",
  "timestamp": "2026-01-15T10:30:00Z"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `status` | string | Always `"ERROR"` for failed requests |
| `message` | string | Human-readable error description |
| `requestId` | string | Unique UUID for the request — **always include this when contacting support** |
| `timestamp` | string | ISO 8601 timestamp of when the error occurred |


requestId
The `requestId` is your most valuable debugging tool. It allows the eCommunicate support team to trace your exact request through the system. Always log it and include it in support tickets.

## HTTP status codes

| Code | Status | Description | Action |
|  --- | --- | --- | --- |
| `200` | OK | Request succeeded | Process the response |
| `202` | Accepted | Message queued for delivery | Track delivery via webhooks or reports |
| `400` | Bad Request | Malformed request or missing required fields | Validate your request payload |
| `401` | Unauthorized | Missing or invalid API key | Verify your API key in the web UI or via [`retrieveApiKey`](/guides/authentication#retrieve-your-api-key), then check the `Authorization` header — see [401 Unauthorized](#401-unauthorized) |
| `403` | Forbidden | Valid key but insufficient permissions | Contact support about account permissions |
| `404` | Not Found | Endpoint or resource does not exist | Verify the URL and resource ID |
| `429` | Too Many Requests | Rate limit exceeded | Implement exponential backoff — see [Rate Limits](/guides/rate-limits) |
| `500` | Internal Server Error | Server-side error | Retry with exponential backoff; contact support if persistent |
| `502` | Bad Gateway | Transient upstream error | Retry with exponential backoff |
| `503` | Service Unavailable | Server temporarily overloaded | Retry with exponential backoff — see [Rate Limits](/guides/rate-limits) |


## Common error scenarios

### 400 Bad Request

The most common error. Check these causes:

Invalid Parameters
- Missing required fields (`sender`, `messages`, `templateName` for notifications)
- Invalid `messageType` value — must be `NOTIFICATION`, `CHAT`, or `CONVERSATION`
- Template body has `{{n}}` placeholders but no `body` component provided
- Invalid phone number format when addressing by phone — international format, leading `+` optional, no leading `0` (e.g. `+27...` or `27...`)
- `channel` field not set to `WHATSAPP` or `SMS` on the unified endpoint
- `templateId` does not match an existing approved template
- Empty `recipients` array, or a recipient with **neither** `to` **nor** `customerUserId` — supply at least one


A malformed BSUID fails as a missing recipient
`customerUserId` is validated against the `^[A-Z]{2}.[0-9]+$` format. A malformed BSUID is **silently ignored**, so a recipient sent with only a bad `customerUserId` (and no `to`) fails as though it had no recipient at all — the error won't mention the BSUID. Double-check the BSUID format before sending phone-less recipients.

### 401 Unauthorized

A `401` always means the same thing: the API key the server received is not one it recognises. The
response body carries `"message": "Unauthorized user"`.

```json
{
  "status": "ERROR",
  "message": "Unauthorized user",
  "requestId": "c3a1f8e2-4b6d-11ee-be56-0242ac120002",
  "timestamp": "2026-01-15T10:30:00Z"
}
```

First step on any 401: verify the key itself
Before you change anything in your code, confirm which key your account actually has. There are two
ways to check, and they should agree:

1. **In the eCommunicate web UI** — sign in and open your API key settings to read the current key.
2. **Via the API** — call [`POST .../authenticate/retrieveApiKey`](/guides/authentication#retrieve-your-api-key)
with your email, password and `clientRef`. It needs no existing key, so it works even when every
other call is returning `401`.


Compare the result against the key your integration is sending. Most `401`s are resolved right here —
the key in the environment is stale, truncated, or belongs to a different client.

Once you've confirmed the key, work through the causes below:

| Cause | Fix |
|  --- | --- |
| API key missing from header | Add `Authorization: YOUR_API_KEY` to every request |
| `Bearer` prefix added | Pass the key on its own — the API uses a custom `apiKey` scheme, not bearer auth |
| API key was regenerated | Use the new key — old keys are permanently invalidated |
| API key is malformed | Verify the key is a valid UUID |
| Key belongs to a different client | Retrieve the key for the `clientRef` you're integrating against |
| Whitespace or newline in the key | Trim the value — a trailing newline from a file or shell variable invalidates it |


**Check what you're actually sending.** Log the header length and last four characters (never the whole
key) to catch truncation and stray whitespace:

Java
```java
String apiKey = System.getenv("ECOMM_API_KEY");

if (apiKey == null || apiKey.isBlank()) {
    throw new IllegalStateException("ECOMM_API_KEY is not set");
}
apiKey = apiKey.trim();

// Safe to log — never log the full key
logger.info("Using API key length={} suffix=...{}",
    apiKey.length(), apiKey.substring(apiKey.length() - 4));

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(BASE_URL + "/whatsapp-api-service/v2/messages"))
    .header("Authorization", apiKey)   // no "Bearer " prefix
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();
```

Node.js
```javascript
const apiKey = (process.env.ECOMM_API_KEY ?? '').trim();

if (!apiKey) throw new Error('ECOMM_API_KEY is not set');

// Safe to log — never log the full key
console.log(`Using API key length=${apiKey.length} suffix=...${apiKey.slice(-4)}`);

const response = await fetch(`${BASE_URL}/whatsapp-api-service/v2/messages`, {
  method: 'POST',
  headers: { 'Authorization': apiKey, 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});
```

Python
```python
api_key = os.environ.get("ECOMM_API_KEY", "").strip()

if not api_key:
    raise RuntimeError("ECOMM_API_KEY is not set")

# Safe to log — never log the full key
print(f"Using API key length={len(api_key)} suffix=...{api_key[-4:]}")

response = requests.post(
    f"{BASE_URL}/whatsapp-api-service/v2/messages",
    headers={"Authorization": api_key, "Content-Type": "application/json"},
    json=payload,
)
```

A UUID key is 36 characters. Any other length means the value was truncated or padded somewhere
between your secret store and the request.

401 vs 403
`401` means the key wasn't recognised — fix the key. `403` means the key **was** recognised but isn't
allowed to do what you asked — fix the permissions, not the key. Retrieving the key again won't help
with a `403`.

### 403 Forbidden

| Cause | Fix |
|  --- | --- |
| Insufficient permissions | Contact support to verify your account role and permissions |
| Wrong client context | You may be attempting to access resources belonging to a different client |


### 404 Not Found

| Cause | Fix |
|  --- | --- |
| Incorrect endpoint URL | Double-check the path against the API Reference |
| Invalid resource ID | Verify the template, webhook, conversation, or ticket ID exists |


## Message delivery failures

When a message is accepted (`202`) but fails to deliver, the failure is reported via [webhooks](/guides/webhooks) (in the `statusUpdateURL` payload) or the Message Reports API.

### Delivery failure codes

| Code | Meaning | Recommended action |
|  --- | --- | --- |
| `4` | Invalid number | Verify the phone number format and that it's a real number |
| `22` | Blacklisted number | The recipient has been blacklisted — check your blacklist settings |
| `23` | Blocked number | The recipient has blocked your number |
| `54` | Non-WhatsApp number | Recipient is not on WhatsApp — enable SMS fallback. SMS fallback needs a phone number, so it isn't available for a recipient addressed by `customerUserId` (BSUID) alone |
| `56` | Failed | Generic delivery failure — check the `failureReason` field for details |
| `58` | Duplicate message | This message was already sent — check your deduplication logic |
| `59` | Sender not connected | WhatsApp Lite sender is disconnected — reconnect via QR code |
| `60` | WhatsApp Lite server unavailable | The Lite server is temporarily down — retry later |
| `130429` | Rate limit hit | You're sending too fast — implement throttling and backoff |
| `131026` | Message undeliverable | Meta could not deliver the message — verify the recipient and template |
| `131047` | Re-engagement required | 24-hour session expired — send a template message to re-engage |
| `131053` | Media upload failed | The media URL is unreachable or the file is too large |
| `132000` | Template paused | Your template has been paused due to quality issues |
| `132001` | Template disabled | Your template has been permanently disabled |


## Troubleshooting flowchart

Use this decision tree when a message fails:

1. **Check the HTTP status code**
  - `4xx` → Fix the request (see scenarios above)
  - `5xx` → Retry with exponential backoff
  - `202` → Message was accepted; check delivery status below
2. **Check delivery status** via webhook or Message Reports API
  - `DELIVERED` / `READ` → Success
  - `FAILED` → Check `failureCode` and `failureReason`
3. **Check the failure code**
  - Codes `4`, `54` → Phone number issue
  - Codes `22`, `23` → Recipient blocked/blacklisted
  - Codes `59`, `60` → WhatsApp Lite connection issue
  - Codes `130xxx` → Meta Cloud API issue
  - Codes `132xxx` → Template issue
4. **If still stuck** → Contact [support@ecommunicate.co.za](mailto:support@ecommunicate.co.za) with the `requestId`


## Retry strategy

For transient errors (`429`, `500`, `502`, `503`), implement exponential backoff:

```javascript
async function sendWithRetry(payload, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await sendMessage(payload);
      if (response.status === 429 || response.status >= 500) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response;
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
      console.log(`Retry ${attempt + 1} in ${delay}ms: ${error.message}`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

Do not retry 4xx errors
Client errors (`400`, `401`, `403`, `404`) indicate a problem with your request. Retrying will produce the same result. Fix the request first.