The error envelope
Every failed request returns the same shape, regardless of endpoint:successis alwaysfalseon an error.error.codeis a stable, machine-readable string — branch your error handling on this, not onmessage.error.messageis human-readable and safe to log or surface in a UI, but isn’t guaranteed to be stable wording across API versions.error.detailsis present on some4xxerrors (for example a list of missing fields) and always absent on any5xx— 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
Next steps
- Rate Limits for backoff strategy detail.
- Core Concepts → Idempotency for the full retry-safety mechanism.
- FAQ & Troubleshooting for setup and integration issues beyond individual error codes.
- Support if you’re stuck on an error this page doesn’t explain.

