> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spenza.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Guides

> Deeper, task-oriented walkthroughs for the most common integration scenarios.

These guides build on the **[Quickstart](/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.

```js theme={null}
async function pollTransaction(statusEndpoint, { intervalMs = 5000, timeoutMs = 60 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(`https://api.spenza.com${statusEndpoint}`);
    const { data } = await res.json();
    if (data.status === "COMPLETED") return data.result;
    if (data.status === "FAILED") throw new Error(`Transaction failed: ${data.errorCode} — ${data.error}`);
    if (data.status === "SCHEDULED") return data; // scheduled for a future date, not an error
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error("Timed out waiting for transaction to complete");
}
```

<Note>
  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.
</Note>

## 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):

```bash theme={null}
curl -X POST "https://api.spenza.com/api/v1.1/esim" \
  -H "Authorization: Bearer $SPENZA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "imei": "356938035643809",
    "simId": "BIB009A",
    "email": "jane@example.com"
  }'
```

**eSIM + plan in one operation:**

```bash theme={null}
curl -X POST "https://api.spenza.com/api/v1.1/esim" \
  -H "Authorization: Bearer $SPENZA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "imei": "356938035643809",
    "simId": "BIB009A",
    "planId": "AT0001",
    "activateNow": true,
    "user": { "name": "Jane Doe", "email": "jane@example.com", "zipcode": "10011" }
  }'
```

Both return `202 Accepted` with `{ transactionId, statusEndpoint }`. Poll it with the pattern above; the completed `result` gives you the provisioned line:

```json theme={null}
{ "mdn": "+12792044584", "iccid": "89012345678901234567", "qrCode": "https://…/qrcode.png" }
```

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:

```bash theme={null}
curl "https://api.spenza.com/api/v1.1/esim/89012345678901234567/qr" \
  -H "Authorization: Bearer $SPENZA_TOKEN"
```

## Managing the SIM lifecycle

A typical SIM's lifecycle through the API:

```text theme={null}
purchase/provision → assign to user → subscribe to a plan → monitor usage → (renew number | cancel plan) → unassign/deactivate
```

* **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) or `POST /api/v3/plans/purchase` (asynchronous, different request contract — see the API Reference).
* **Monitor** — `GET /api/v1.1/sims/{iccid}/usage` for the current cycle; pass `history=true` for prior cycles.
* **Renew the phone number** — `POST /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 subscription** — `POST /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`:

```bash theme={null}
curl -X POST "https://api.spenza.com/api/v1.1/sim-groups" \
  -H "Authorization: Bearer $SPENZA_TOKEN" -H "Content-Type: application/json" \
  -d '{ "name": "Sales Team", "spendLimit": 1000, "dataQuota": 100, "currency": "USD", "dataUnit": "GB" }'

curl -X POST "https://api.spenza.com/api/v1.1/sim-groups/Sales%20Team/sims" \
  -H "Authorization: Bearer $SPENZA_TOKEN" -H "Content-Type: application/json" \
  -d '{ "iccid": "8901260853182965429" }'
```

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:

```bash theme={null}
curl -X POST "https://api.spenza.com/api/v1.1/port-in/eligibility" \
  -H "Authorization: Bearer $SPENZA_TOKEN" -H "Content-Type: application/json" \
  -d '{ "portInNumber": "+15555551234", "plan": "AT0001" }'
```

<Note>
  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.
</Note>

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:

```bash theme={null}
curl -X POST "https://api.spenza.com/api/v3/port-in" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "AT0001",
    "portInNumber": "+15555551234",
    "currentCarrierName": "Verizon",
    "currentAccountNumber": "1234567890",
    "currentAccountPassword": "1234",
    "currentBillingAddress": { "street1": "123 Main St", "city": "New York", "state": "NY", "zip": "10011" },
    "employeeEmail": "jane@example.com",
    "employeeName": "Jane Doe"
  }'
```

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](/environments-versioning#one-documented-exception-port-in-submission)** 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:

```bash theme={null}
curl "https://api.spenza.com/api/v1.1/port-ins?status=IN_PROGRESS" \
  -H "Authorization: Bearer $SPENZA_TOKEN"
```

## 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:

```bash theme={null}
curl "https://api.spenza.com/api/v1.1/invoices?from=2026-06-01&to=2026-06-30" \
  -H "Authorization: Bearer $SPENZA_TOKEN"
```

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:

```bash theme={null}
curl "https://api.spenza.com/api/v1.1/credit-balance" -H "Authorization: Bearer $SPENZA_TOKEN"

curl -X POST "https://api.spenza.com/api/v1.1/top-up" \
  -H "Authorization: Bearer $SPENZA_TOKEN" -H "Content-Type: application/json" \
  -d '{ "amount": 100, "currency": "usd" }'
```

`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](/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](/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](/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](/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](/webhooks)**.
* [ ] You have a support contact path for issues — see **[Support](/support)**.
