> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spenza.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The standard error envelope, the full error-code taxonomy, and how to handle each one.

## The error envelope

Every failed request returns the same shape, regardless of endpoint:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "SIM_NOT_FOUND",
    "message": "We couldn't find a SIM with that ICCID on your account.",
    "details": { "field": "iccid" }
  }
}
```

* **`success`** is always `false` on an error.
* **`error.code`** is a stable, machine-readable string — branch your error handling on this, not on `message`.
* **`error.message`** is human-readable and safe to log or surface in a UI, but isn't guaranteed to be stable wording across API versions.
* **`error.details`** is present on some `4xx` errors (for example a list of missing fields) and always **absent on any `5xx`** — Spenza never includes structured internal detail in a server-error response.

## Error codes

Generic, HTTP-status-driven codes used across most endpoints:

| HTTP | `error.code`             | Meaning                                                                                                                          |
| ---- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `VALIDATION_ERROR`       | A field failed a format/constraint check, or (on some endpoints) a business-rule check like a bad date range.                    |
| 400  | `MISSING_FIELDS`         | One or more required fields are absent — check `error.details` when present.                                                     |
| 401  | `UNAUTHORIZED`           | The bearer token is missing, malformed, or expired — or, at the token-exchange step, the key/secret pair was wrong.              |
| 403  | `FORBIDDEN`              | Your account's plan doesn't include this feature, or the token lacks the role tier the endpoint requires.                        |
| 404  | `NOT_FOUND`              | Generic "doesn't exist under your account" — used where no more specific code applies (for example `GET /api/v1.1/orders/{id}`). |
| 409  | `CONFLICT`               | A state conflict — for example a user with that email already exists, or a device is already assigned.                           |
| 409  | `IDEMPOTENCY_KEY_REUSED` | An `Idempotency-Key` was reused with a different request body, or against a different route.                                     |
| 429  | `RATE_LIMITED`           | You've exceeded a per-endpoint or account-wide rate limit.                                                                       |
| 500  | `INTERNAL_ERROR`         | An unexpected server-side error.                                                                                                 |
| 501  | `NOT_IMPLEMENTED`        | The action isn't supported for this account's carrier/operator (for example outbound SMS on a carrier that doesn't support it).  |

Domain-specific `404` codes, one per resource: `SIM_NOT_FOUND`, `SUBSCRIPTION_NOT_FOUND`, `PLAN_NOT_FOUND`, `INVOICE_NOT_FOUND`, `USER_NOT_FOUND`, `DEVICE_NOT_FOUND`, `WEBHOOK_NOT_FOUND`, `DELIVERY_NOT_FOUND`, `NOTIFICATION_NOT_FOUND`, `ADMIN_NOT_FOUND`, `SIM_GROUP_NOT_FOUND`, `DEVICE_GROUP_NOT_FOUND`, `USER_GROUP_NOT_FOUND`.

Group and membership codes: `SIM_GROUP_ALREADY_EXISTS` / `SIM_GROUP_NOT_EMPTY` / `SIM_NOT_IN_GROUP`, `DEVICE_GROUP_ALREADY_EXISTS` / `DEVICE_GROUP_NOT_EMPTY` / `DEVICE_NOT_IN_GROUP`, `USER_GROUP_ALREADY_EXISTS` / `USER_GROUP_NOT_EMPTY` / `USER_NOT_IN_GROUP`, `CANNOT_DELETE_DEFAULT_GROUP`.

Device codes: `DEVICE_ALREADY_ASSIGNED`, `DEVICE_NOT_ASSIGNED`.

Team Member (admin) codes: `INVALID_ROLE` (422), `INVALID_DEPARTMENT` (422), `ADMIN_ALREADY_EXISTS` (409), `CANNOT_DEMOTE_LAST_ADMIN` (400), `CANNOT_REMOVE_LAST_ADMIN` (400), `CANNOT_REMOVE_SELF` (400).

Webhook delivery codes: `DELIVERY_FAILED` (502 — your endpoint didn't respond successfully to a test), `DELIVERY_ALREADY_IN_PROGRESS` (409).

Billing/purchase code: `PRICING_NOT_CONFIGURED` — the plan has no usable price configured for your account; this is a merchant-configuration gap, not something your request caused. Contact support.

<Note>
  This is the exception-driven subset actually thrown by the API today. `GET /api/v3/transactions/{transactionId}` (the async status poll) can surface any of the domain-specific `404` codes above as a `FAILED` transaction's `errorCode`, plus `VALIDATION_ERROR`, `MISSING_FIELDS`, `CONFLICT`, `RATE_LIMITED`, `NOT_IMPLEMENTED`, `PRICING_NOT_CONFIGURED`, or the default `INTERNAL_ERROR`.
</Note>

## Troubleshooting by code

**`400 MISSING_FIELDS` / `VALIDATION_ERROR`**
Read `error.details` when present — it often names the offending field(s). Cross-check the field against the **Body**/**Query**/**Path parameters** tables on that operation's reference page for the correct type, required-ness, and enum values. These are client-side bugs; fix the request rather than retrying it unchanged.

**`401 UNAUTHORIZED`**
Either your key/secret pair was wrong at `POST /api/v1.1/auth/token`, or your bearer token is missing, malformed, or past its `expires_at` on a later call. See **[Authentication](/authentication)**.

**`403 FORBIDDEN`**
Either your account's plan doesn't include this capability, or your token doesn't have the role tier the endpoint needs. Contact Spenza support to confirm what your account/integration needs.

**`404 *_NOT_FOUND`**
The identifier in your path doesn't exist under your account. Confirm you're using the right identifier (users and team members are looked up by **email**, groups by **name**, not an internal ID) and that the resource wasn't already deleted/removed by a prior call. Most `404`s here are deliberately indistinguishable between "doesn't exist" and "belongs to another account" — this is an intentional account-enumeration guard, not a bug.

**`409 CONFLICT`**
Something you're trying to create already exists, or the resource is in a state that blocks the action (a duplicate user email, an already-assigned device, an already-cancelled subscription). Fetch the current state instead of retrying blindly.

**`409 IDEMPOTENCY_KEY_REUSED`**
You reused an `Idempotency-Key` with a different request body (or on a different route) from the original call that used it. Generate a new key per logical request — see **[Core Concepts → Idempotency](/core-concepts#idempotency)**.

**`429 RATE_LIMITED`**
Back off and retry later — see **[Rate Limits](/rate-limits)** for the recommended strategy. Don't tighten a retry loop in response to this; that makes it worse.

**`500 INTERNAL_ERROR`**
An unexpected failure on Spenza's side. Safe to retry with backoff for read (`GET`) requests. For write requests, prefer sending an `Idempotency-Key` up front so a retry is safe by construction — see below.

## Retry recommendations

| Status                                            | Retry?                                                                                 | Notes                                                                                                     |
| ------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| 400, 403, 404, 409 (not `IDEMPOTENCY_KEY_REUSED`) | **No**                                                                                 | These indicate a problem with the request or your account state, not a transient failure. Fix and resend. |
| 401 (expired token)                               | **Yes, after re-authenticating**                                                       | Not a blind retry — get a fresh token first.                                                              |
| 429                                               | **Yes, with backoff**                                                                  | See **[Rate Limits](/rate-limits)**.                                                                      |
| 500                                               | **Yes, with backoff — GET requests safely; POST/PUT/DELETE with an `Idempotency-Key`** | See below.                                                                                                |

<Tip>
  **Use `Idempotency-Key` instead of guessing.** Many write endpoints (purchases, assignments, group membership changes, and more — see each operation's page) accept an `Idempotency-Key` header, which makes a retry after a timeout or `5xx` safe by construction: the same key + same body replays the original result rather than repeating the action. Where an endpoint doesn't support it, check current state first (for example, `GET /api/v1.1/sims/{iccid}/subscription` before retrying a plan purchase) rather than re-issuing the write unconditionally.
</Tip>

## Next steps

* **[Rate Limits](/rate-limits)** for backoff strategy detail.
* **[Core Concepts → Idempotency](/core-concepts#idempotency)** for the full retry-safety mechanism.
* **[FAQ & Troubleshooting](/faq)** for setup and integration issues beyond individual error codes.
* **[Support](/support)** if you're stuck on an error this page doesn't explain.
