Skip to main content

The error envelope

Every failed request returns the same shape, regardless of endpoint:
  • 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: 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.
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.

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. 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 404s 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. 429 RATE_LIMITED Back off and retry later — see 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

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.

Next steps