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. - Toggle voice/SMS/data —
PATCH /api/v1.1/sims/{iccid}/service-stateto enable or disable voice, SMS, and/or data on the line without cancelling the subscription itself. Each is independently optional (send just the ones you want to change), but togglingvoice/smsstill requires the active plan to include that capability, anddataisn’t controllable by any operator yet — see the API Reference for the exactwarningsbehavior on a mixed request. - 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 call quality
Find out how good a real phone call from one of your lines is. Register a webhook, call a Spenza test number from a line on your account, and when the call ends Spenza sends a scored report — packet loss, jitter, round-trip time and a MOS score for each direction of the audio — to your webhook.Spenza only receives test calls; it never calls your lines. A report is delivered only when the calling number is a line on your account — a call from any other number is measured but never sent to you.
https on port 443 and publicly reachable; you can register up to 10.
signingSecret once — store it to verify deliveries.
2. Call the test number. Call +1 279 204 4328 from a line on your account and stay on the line for about 20 seconds; a call too short to measure comes back INCONCLUSIVE.
3. Receive voice.test.completed. Shortly after you hang up:
X-Spenza-Signature as described in Verifying delivery signatures, using this webhook’s signingSecret. Success is any 2xx within 10 seconds; a failed delivery is retried after 30 seconds, 2 minutes and 10 minutes (4 attempts), and 410 Gone stops retries. Dedupe on id.
Reading the report. node is Spenza’s voice server and caller is the phone that dialled, so node_to_caller is the audio Spenza sent towards your line and caller_to_node is the audio your line sent to Spenza.
The score is calculated from how the audio packets travelled, not from listening to the call, so a faulty microphone or speaker isn’t detected. It measures as far as the carrier equipment on your line’s side. A jitter of exactly
0 usually means your carrier recalculates these reports rather than passing raw figures through — that’s normal. Place test calls one after another rather than all at once.
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.

