<!-- VanceTel docs · /docs/errors -->

# Errors

The API uses conventional HTTP status codes to signal success or failure, and returns a consistent JSON **error object** on every failed request. Read the status code first to decide what to do, then read the error body for detail.

## The error object

Every error response has the same shape: a top-level `error` object with a machine-readable `type` and a human-readable `message`.

```json
{
  "error": {
    "type": "invalid_request",
    "message": "field 'to' must be an E.164 phone number"
  }
}
```

- **`type`** — a stable string you can branch on in code (e.g. `invalid_request`). Match on this, not on the message text.
- **`message`** — a description meant for logs and developers. It is not localized and may change; never parse it.

  Branch your code on the HTTP status code and on `error.type`. Treat `error.message` as a
  human-readable hint only — its wording is not part of the contract.

## Status codes

| Status | Meaning | When it occurs |
| --- | --- | --- |
| `200 OK` | Success | A `GET` succeeded, e.g. retrieving a [message](/developers/docs/sms) or [contact](/developers/docs/contacts). |
| `201 Created` | Success | A resource was created, e.g. a message was accepted for delivery or a contact was created. |
| `400 Bad Request` | Client error | The request was malformed — invalid JSON, or a missing/unsupported `Content-Type`. |
| `401 Unauthorized` | Auth error | The API key is missing, malformed, or revoked. See [Authentication](/developers/docs/authentication). |
| `404 Not Found` | Client error | The resource ID does not exist or is not on your account (e.g. an unknown message ID). |
| `422 Unprocessable Entity` | Validation error | The request was well-formed but failed validation — e.g. a non-[E.164](/developers/glossary/e164) number or an empty `body`. |
| `429 Too Many Requests` | Rate limited | You exceeded your request rate. Back off and retry — see [Rate limits](/developers/docs/rate-limits). |
| `500`–`503` | Server error | Something went wrong on our side. These are transient; retry with backoff. |

  A `2xx` for `POST /messages` means the message was **accepted for delivery**, not that it has
  reached the handset. Final delivery is reported asynchronously via the message `status` field
  and delivery-receipt [webhooks](/developers/docs/webhooks).

## Example responses

A validation failure on `POST /messages` returns `422` with a `validation_error` type:

```json
{
  "error": {
    "type": "validation_error",
    "message": "field 'to' is required"
  }
}
```

A missing or revoked key returns `401`:

```json
{
  "error": {
    "type": "unauthorized",
    "message": "missing or invalid API key"
  }
}
```

Requesting an unknown ID from `GET /messages/{id}` returns `404`:

```json
{
  "error": {
    "type": "not_found",
    "message": "no message with id 'msg_does_not_exist'"
  }
}
```

## Handling errors

Check the status code, then read `error.type` for cases you handle explicitly. The pattern below treats `4xx` as caller mistakes to fix, `429` and `5xx` as retryable, and surfaces the error object on everything else.

**Node**

```ts
async function sendMessage(payload) {
  const res = await fetch("https://api.netexem.com/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });

  if (res.ok) return res.json();

  const { error } = await res.json();

  if (res.status === 401) {
    throw new Error(`Auth failed: ${error.message}`);
  }
  if (res.status === 422 || res.status === 400) {
    // Caller mistake — fix the request, do not retry as-is.
    throw new Error(`Invalid request (${error.type}): ${error.message}`);
  }
  if (res.status === 429 || res.status >= 500) {
    // Transient — safe to retry with backoff. See /docs/rate-limits.
    throw new Error(`Retryable error (${res.status}): ${error.message}`);
  }

  throw new Error(`Unexpected ${res.status}: ${error.message}`);
}
```

**Python**

```python
import os, requests

def send_message(payload):
    res = requests.post(
        "https://api.netexem.com/v1/messages",
        headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"},
        json=payload,
    )

    if res.ok:
        return res.json()

    error = res.json()["error"]

    if res.status_code == 401:
        raise RuntimeError(f"Auth failed: {error['message']}")
    if res.status_code in (400, 422):
        # Caller mistake — fix the request, do not retry as-is.
        raise ValueError(f"Invalid request ({error['type']}): {error['message']}")
    if res.status_code == 429 or res.status_code >= 500:
        # Transient — safe to retry with backoff. See /docs/rate-limits.
        raise RuntimeError(f"Retryable error ({res.status_code}): {error['message']}")

    raise RuntimeError(f"Unexpected {res.status_code}: {error['message']}")
```

### Which errors to retry

- **Do not retry** `400`, `401`, `404`, or `422` — the request will fail again until you fix it.
- **Retry** `429` and `5xx` with exponential backoff. For `429`, honor the `Retry-After` header. See [Rate limits](/developers/docs/rate-limits) for the recommended loop.

  Log the full error object (`type` and `message`) alongside the request ID from your own
  system. The `type` lets you aggregate failures; the `message` speeds up debugging.

## Next steps

- [Rate limits](/developers/docs/rate-limits) — handling `429` and backing off correctly
- [Authentication](/developers/docs/authentication) — avoiding `401` errors
- [API reference](/developers/docs/api-reference) — try requests live and see real responses
