Handling async operations
Some operations don’t complete instantly — provisioning an eSIM involves an operator round-trip that can take up to ~45 minutes; a port-in depends on the losing carrier’s response time. Both of those, plus the asynchronous purchase variants and top-up, return atransactionId immediately instead of blocking:
- Call the endpoint. You get back
202(or201for top-up) with{ transactionId, statusEndpoint }. - Poll
statusEndpoint(GET /api/v3/transactions/{transactionId}). No auth header needed — the transaction ID is itself the credential. - Stop polling once
statusis terminal.COMPLETED,FAILED, orSCHEDULEDare terminal;PENDINGandPROCESSINGmean keep polling.
Poll every ~5 seconds for most operations; eSIM provisioning and port-in are slower-moving, so a ~30 second interval is more appropriate for those to avoid hammering the endpoint for no benefit.
Provisioning an eSIM end-to-end
One endpoint provisions the eSIM, with or without a plan attached — includeplanId to assign one in the same operation instead of purchasing it separately afterward. This endpoint is always asynchronous, regardless of whether a plan is included:
eSIM only (SIM-only provisioning is currently supported for the SpenzaJ carrier family only — other carriers return 501 NOT_IMPLEMENTED on this branch):
202 Accepted with { transactionId, statusEndpoint }. Poll it with the pattern above; the completed result gives you the provisioned line:
Managing the SIM lifecycle
A typical SIM’s lifecycle through the API:- Assign —
POST /api/v1.1/sims/{iccid}/assign. Reassigning an already-assigned SIM is supported directly — the previous holder is replaced, not blocked. - Subscribe —
POST /api/v1.1/plans/purchase(synchronous) orPOST /api/v3/plans/purchase(asynchronous, different request contract — see the API Reference). - Monitor —
GET /api/v1.1/sims/{iccid}/usagefor the current cycle; passhistory=truefor prior cycles. - Renew the phone number —
POST /api/v1.1/sims/{iccid}/renew-numberif the assigned number needs to change. This is rate-limited (20/min) — handle429 RATE_LIMITEDgracefully rather than retrying immediately. - Cancel a subscription —
POST /api/v1.1/subscriptions/{iccid}/cancel. Synchronous; cancels at end of the current billing period by default, so anACTIVEsubscription typically comes backCANCEL_SCHEDULEDrather thanCANCELLEDimmediately. Note this endpoint returns201, not200, on success.
Organizing your fleet with groups
SIMs, devices, and users can each be organized into named groups (with an optionalspendLimit, dataQuota, currency, and dataUnit) — separate resources under /api/v1.1/sim-groups, /api/v1.1/device-groups, and /api/v1.1/user-groups:
name is its identifier in the URL — URL-encode it if it contains spaces or special characters, and note it can’t be renamed once created (the update endpoints don’t accept a new name). Removing a SIM from its group moves it back to the account’s default sim-group (movedTo: "default"); removing a device or user from its group simply clears the assignment (movedTo: null) — there’s no default group for those two.
Porting in an existing number
Check eligibility first — it’s a read-only call, no order is created:Only the SpenzaJ carrier currently implements this check — every other carrier returns
501 NOT_IMPLEMENTED, so don’t treat that as a hard failure signal for carriers you know support port-in generally.POST /api/v1.1/port-ins. The only submission endpoint is:
Admin/Super Admin/Standard Admin role rather than just a valid bearer token, and its success response is a flat shape — { success, transactionId, statusEndpoint, message, timestamp }, no data wrapper — pointing at the legacy singular status path. See Environments & Versioning → One documented exception before you build against it. currentAccountPassword is the losing carrier’s account PIN required for FCC-compliant number portability (LNP) — handle it with the same care as any other credential; never log it.
Track status separately with:
Reconciling billing
Pull invoices on a schedule (for example, monthly) to reconcile against your own finance system.from/to here are date-only (YYYY-MM-DD) — unlike Orders/Transactions, which take full datetimes:
lineItems and a usageSummary. To get the underlying PDF, follow the redirect from GET /api/v1.1/invoices/{id}/pdf — it 302s to a signed download URL (an S3-hosted PDF, or a Stripe receipt URL as a fallback when no stored PDF exists yet).
Check prepaid credit and top it up the same way:
top-up returns a checkoutUrl (hosted checkout) alongside a transactionId/statusEndpoint — redirect the user there to complete payment; poll the transaction to confirm it landed.
Integrating webhooks
Register a webhook, then verify every delivery’s signature before trusting it — see Webhooks for the full registration flow, the delivery-signing scheme, and how to inspect/redeliver past events.Testing and validation guidance
There’s no separate sandbox documented for this API (see Environments & Versioning), so validate your integration carefully against production:- Start with read-only calls (
GET /api/v1.1/sims,GET /api/v1.1/plans) to confirm authentication and pagination before writing any code that mutates state. - Use a small number of real-but-low-value test resources (a single spare SIM, a test user) rather than scripting bulk writes against production inventory.
- Use an
Idempotency-Keyon write calls that support it — see Core Concepts → Idempotency — so a client-side retry after a timeout can’t duplicate the action. - Validate request bodies client-side against the field tables in the API Reference — a
400 VALIDATION_ERROR/MISSING_FIELDSon a write call is cheaper to catch locally than after Spenza has already tried to place an order. - For destructive or billable actions (plan purchases, top-ups, port-ins), gate them behind an explicit confirmation step in your own tooling until you’re confident in the integration.
Production-readiness checklist
- Credentials are stored in a secrets manager, not in code or
.envfiles committed to git. - Your client re-authenticates automatically on
401 UNAUTHORIZEDrather than crashing. - Every write call has retry logic for
429/5xxwith backoff (see Rate Limits), and does not blindly retry4xxvalidation errors. - Write calls that support it send an
Idempotency-Key, so a retry after a timeout can’t duplicate a purchase. - Async operations are polled with a bounded timeout, not an infinite loop.
- Webhook endpoints (if used) verify
X-Spenza-Signatureand are idempotent against redelivery — see Webhooks. - You have a support contact path for issues — see Support.

