Skip to main content
These guides build on the Quickstart and assume you can already authenticate and make a basic call. Each one is a self-contained scenario you can implement independently.

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 a transactionId immediately instead of blocking:
  1. Call the endpoint. You get back 202 (or 201 for top-up) with { transactionId, statusEndpoint }.
  2. Poll statusEndpoint (GET /api/v3/transactions/{transactionId}). No auth header needed — the transaction ID is itself the credential.
  3. Stop polling once status is terminal. COMPLETED, FAILED, or SCHEDULED are terminal; PENDING and PROCESSING mean 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 — include planId 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):
eSIM + plan in one operation:
Both return 202 Accepted with { transactionId, statusEndpoint }. Poll it with the pattern above; the completed result gives you the provisioned line:
Once provisioning completes, fetch the QR code / activation code again at any time — useful if the user needs to re-scan on a new device:

Managing the SIM lifecycle

A typical SIM’s lifecycle through the API:
  • AssignPOST /api/v1.1/sims/{iccid}/assign. Reassigning an already-assigned SIM is supported directly — the previous holder is replaced, not blocked.
  • SubscribePOST /api/v1.1/plans/purchase (synchronous) or POST /api/v3/plans/purchase (asynchronous, different request contract — see the API Reference).
  • MonitorGET /api/v1.1/sims/{iccid}/usage for the current cycle; pass history=true for prior cycles.
  • Renew the phone numberPOST /api/v1.1/sims/{iccid}/renew-number if the assigned number needs to change. This is rate-limited (20/min) — handle 429 RATE_LIMITED gracefully rather than retrying immediately.
  • Cancel a subscriptionPOST /api/v1.1/subscriptions/{iccid}/cancel. Synchronous; cancels at end of the current billing period by default, so an ACTIVE subscription typically comes back CANCEL_SCHEDULED rather than CANCELLED immediately. Note this endpoint returns 201, not 200, on success.

Organizing your fleet with groups

SIMs, devices, and users can each be organized into named groups (with an optional spendLimit, dataQuota, currency, and dataUnit) — separate resources under /api/v1.1/sim-groups, /api/v1.1/device-groups, and /api/v1.1/user-groups:
A group’s 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.
Submitting the port-in itself is a documented exception to the rest of this API — there is no POST /api/v1.1/port-ins. The only submission endpoint is:
This one requires your account’s 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:
Each invoice includes 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-Key on 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_FIELDS on 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 .env files committed to git.
  • Your client re-authenticates automatically on 401 UNAUTHORIZED rather than crashing.
  • Every write call has retry logic for 429/5xx with backoff (see Rate Limits), and does not blindly retry 4xx validation 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-Signature and are idempotent against redelivery — see Webhooks.
  • You have a support contact path for issues — see Support.