Download OpenAPI specification:
Purchase eSIM plans, manage eSIMs, monitor usage, and control your wallet balance.
Base URL: https://api.esimdatastore.com/api
Welcome to the eSIM Data Store API. This RESTful JSON API lets you browse eSIM plans, purchase eSIMs, monitor data usage, top up active eSIMs, and manage your prepaid wallet balance. The wholesale customer surface documented here is JSON and API-key authenticated. A few sibling surfaces on the same host behave differently and are noted where they appear: the public voucher-redemption and install-landing routes (/r/*, /install/*) are anonymous; admin routes require a bearer token; report endpoints can return text/csv or application/pdf when the caller passes ?format=csv or ?format=pdf on the query string (JSON is the default).
Authenticate every request by including your API key in the X-API-Key header. Each key is scoped to a single customer account — all resources you create and query are automatically isolated to your account.
X-API-Key: your-api-key-here
API keys are provided during onboarding. If you need a key or need to rotate an existing one, contact support.
Requests without a valid API key receive a 401 Unauthorized response. Keep your key secret and never expose it in client-side code.
Follow these steps to make your first eSIM purchase and monitor it.
1. Browse plans. Fetch the list of available eSIM plans with pricing and coverage details.
curl -X GET https://api.esimdatastore.com/api/plans \
-H "X-API-Key: your-api-key"
2. Purchase a plan. Buy an eSIM by submitting an order with the desired planId. Include an Idempotency-Key header to prevent double charges.
curl -X POST https://api.esimdatastore.com/api/orders \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{"planId": "plan-uuid-here"}'
3. Get your eSIM. List your eSIMs, then fetch a specific eSIM to get the esimQr value for device installation.
curl -X GET https://api.esimdatastore.com/api/esims \
-H "X-API-Key: your-api-key"
4. Monitor usage. Check data consumption, activation time, and expiry for any eSIM.
curl -X GET https://api.esimdatastore.com/api/esims/{esim-id}/usage \
-H "X-API-Key: your-api-key"
5. Top up. When data is running low, check available top-up plans and add more data.
# Check available top-ups
curl -X GET https://api.esimdatastore.com/api/esims/{esim-id}/topups/available \
-H "X-API-Key: your-api-key"
# Apply a top-up
curl -X POST https://api.esimdatastore.com/api/esims/{esim-id}/topup \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 660e8400-e29b-41d4-a716-446655440001" \
-d '{"planId": "topup-plan-uuid"}'
6. Check balance. View your prepaid wallet balance. Purchases are deducted automatically.
curl -X GET https://api.esimdatastore.com/api/wallets/balance \
-H "X-API-Key: your-api-key"
To prevent duplicate charges on write operations, include an Idempotency-Key header with a unique value (UUID recommended). When the API sees the same key within a 24-hour retention window, it returns the original cached response instead of re-executing the request. This is the safe way to retry after a network timeout — see the Orders tag for the full list of idempotent endpoints and their per-endpoint semantics.
If a duplicate arrives while the original is still in flight, the API returns 409 Conflict. Reusing the same key with a different request body is undefined behaviour — always mint a fresh UUID per logical operation.
All API errors emit application/problem+json per RFC 7807. The envelope carries type, title, status, detail, and — for machine-driven callers — a code field naming the error family:
{
"type": "https://esimdatastore.com/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "Duplicate request in progress",
"code": "CONFLICT"
}
The code values you should branch on: VALIDATION_ERROR (request shape or field rejection — 422), NOT_FOUND (unknown resource or one that belongs to another account — 404), CONFLICT (idempotency collision, state conflict, in-flight duplicate — 409), UPSTREAM_ERROR (eSIMfx call failed or timed out — 502), RATE_LIMITED (throttle exceeded — 429, always paired with a Retry-After header), UNAUTHORIZED (missing or invalid API key — 401), and INTERNAL_ERROR (uncategorized server failure — 500). Validation errors carry a per-field errors extension; circuit-open 503 and throttled 429 responses carry a retryAfterSeconds extension mirroring the header.
Requests are rate-limited per API key. When a limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header (in seconds) indicating how long to wait before retrying. Typical ceilings are 30 requests/minute for cheap reads, 10/minute for writes and sensitive reads, and 5/minute for refunds — but do not hard-code these numbers; treat the Retry-After header as authoritative.
Implement exponential backoff on 429. Retrying inside the Retry-After window will not succeed and can compound the throttle.
Admin surface for managing per-customer plan pricing. Each customer has an independent rate-card in customer_plan_prices that determines the price returned by GET /plans and the amount debited on POST /orders. Prices are uploaded in bulk via CSV; the upload path also enforces the plan-level margin floor (rate must be greater than or equal to upstream cost) and auto-disables plans that go underwater for a given customer, emitting a NEGATIVE_MARGIN_PLAN critical event.
A customer must have a rate-card row for a plan before that plan is orderable or votable for a voucher batch — the pre-flight check in POST /admin/code-batches returns 422 INVALID_REFERENCE if a row is missing. Requires an admin bearer token.
| customerId required | string |
[- {
- "planId": "string",
- "esimfxProductId": "string",
- "name": "string",
- "disabled": true,
- "disabledReason": { },
- "removedFromUpstream": true,
- "upstreamCost": "string",
- "price": "string",
- "priceDisabled": true,
- "priceDisabledReason": { },
- "zeroMarginAllowed": true
}
]| customerId required | string |
{ }{- "enabled": 0
}Manage the eSIMs on your account. GET /esims lists inventory with status and creation time; GET /esims/:id returns the full record including the LPA esimQr string used for device installation and a live liveStatus block fetched from the upstream provider. GET /esims/:id/usage returns data consumption, activation time, and expiry — polled with a 60-second per-ICCID window to protect the upstream (window-hit returns 429 with Retry-After, never stale data).
Top-ups live under the same resource: GET /esims/:id/topups/available lists the top-up plans valid for a given eSIM, and POST /esims/:id/topup applies one against the ICCID (idempotent, wallet-charged). Sandbox eSIMs always report ACTIVE and return simulated usage.
Activates a PENDING subscription for the given ICCID. Sandbox activations return a simulated ACTIVE status.
| iccid required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}Sandbox eSIMs always show ACTIVE status.
| id required | string |
{- "id": "string",
- "iccid": "string",
- "esimQr": "string",
- "status": "PROVISIONED",
- "createdAt": "2019-08-24T14:15:22Z",
- "liveStatus": {
- "iccid": "string",
- "esimQr": "string",
- "status": "string"
}
}Sandbox returns simulated usage data. Polls the upstream provider at most once per 60 seconds per eSIM (per-ICCID window on EsimUsagePollService). Calls within the 60-second window since the last successful poll receive 429 Too Many Requests with a Retry-After: <seconds> header and a retryAfterSeconds problem+json extension. Integrators should honour Retry-After and cache the response for the interval — polling below the ceiling wastes quota against SENSITIVE_THROTTLE (10 req/min/endpoint) without gaining fresher data.
| id required | string |
{- "iccid": "string",
- "usedAmount": 0,
- "totalAmount": 0,
- "amountUnit": "string",
- "status": "string",
- "activationTime": "string",
- "expiry": "string"
}Sandbox returns all plans with customer pricing (no upstream filter).
| id required | string |
[- {
- "planId": "string",
- "name": "string",
- "duration": 0,
- "durationUnit": "string",
- "amount": 0,
- "amountUnit": "string",
- "coverage": [
- "string"
], - "price": "string",
- "provisioningFee": "0.00"
}
]Sandbox top-ups simulate upstream without real provisioning.
| id required | string |
| planId required | string ID of the top-up plan to apply |
{- "planId": "string"
}{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "9.99",
- "planPrice": "5.99",
- "provisioningFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50"
}Public, anonymous eSIM install surface. GET /install/:token resolves an Ed25519-signed JWT install token and returns the LPA activation string plus a data-URI QR image for one-tap device activation — no Authorization header, no API key, and no ICCID in the response. The token itself is the credential: anyone with the URL can fetch the payload while it is valid, so responses carry Cache-Control: no-store and are throttled per-IP (10/min) and per-token (30/min, SHA-256 hashed).
Install tokens are minted by admin-scoped origination flows (POST /origination/issue) and by anonymous voucher redemption. Malformed or unverifiable tokens return 422 INVALID_REFERENCE; a valid token whose eSIM row no longer exists returns 404 NOT_FOUND; expired eSIMs return 410 GONE; already-redeemed eSIMs return the metadata with blank lpaString and qrDataUri and status: "redeemed".
{- "status": "ready",
- "lpaString": "string",
- "qrDataUri": "string",
- "planName": "string",
- "countryName": "string",
- "dataAmount": "string",
- "validityDays": 0
}Purchase eSIM plans and retrieve order state. POST /orders charges your prepaid wallet, provisions an eSIM upstream, and returns an OrderResponse that tracks the full lifecycle — from PENDING through COMPLETED, REFUND_PENDING, REFUNDED, or one of the terminal failure states. Include an Idempotency-Key header on every write; the same key within 24 hours replays the cached response instead of double-charging.
Fetch a single order via GET /orders/:id or paginate the full list via GET /orders?limit=&cursor= (cursor-based, max 100 per page). Sandbox customers receive dummy eSIMs (ICCID prefix 89990) with no real upstream provisioning.
Sandbox customers receive dummy eSIMs with no real provisioning.
| planId required | string ID of the plan to purchase |
{- "planId": "string"
}{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "9.99",
- "planPrice": "5.99",
- "provisioningFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50"
}[- {
- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "9.99",
- "planPrice": "5.99",
- "provisioningFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50"
}
]{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "9.99",
- "planPrice": "5.99",
- "provisioningFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50"
}Browse the eSIM plan catalog. GET /plans returns the plans that are available to your account — plans that are neither disabled nor removed from upstream, and priced under your customer rate-card. Each PlanResponse carries the display metadata (name, coverage, duration, data allowance), the customer-facing price in USD, and the fixed provisioningFee charged on top of the plan price for NEW orders (top-ups always carry a "0.00" fee). The catalog is the source of planId values you pass to POST /orders and POST /esims/:id/topup.
[- {
- "id": "string",
- "name": "string",
- "description": "string",
- "price": "9.99",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US",
- "provisioningFee": "0.50"
}
][- {
- "id": "string",
- "esimfxProductId": "string",
- "esimfxImsiProfile": "string",
- "name": "string",
- "description": "string",
- "upstreamCost": "string",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US",
- "destination": "string",
- "compatibleTopupProductIds": [
- "string"
], - "disabled": true,
- "disabledReason": "MANUAL",
- "removedFromUpstream": true,
- "createdAt": "2019-08-24T14:15:22Z"
}
]{- "status": "idle",
- "startedAt": "2019-08-24T14:15:22Z",
- "finishedAt": "2019-08-24T14:15:22Z",
- "result": {
- "created": 0,
- "updated": 0,
- "failed": [
- "string"
], - "warnings": [
- "string"
]
}, - "error": { }
}| disabledReason required | string Enum: "MANUAL" "NEGATIVE_MARGIN" "DUPLICATE" Re-enable all currently-disabled plans whose |
{- "disabledReason": "MANUAL"
}{- "enabled": 0,
- "disabledReason": "MANUAL"
}| id required | string |
| disabled required | boolean Whether the plan is disabled |
{- "disabled": true
}{- "id": "string",
- "esimfxProductId": "string",
- "esimfxImsiProfile": "string",
- "name": "string",
- "description": "string",
- "upstreamCost": "string",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US",
- "destination": "string",
- "compatibleTopupProductIds": [
- "string"
], - "disabled": true,
- "disabledReason": "MANUAL",
- "removedFromUpstream": true,
- "createdAt": "2019-08-24T14:15:22Z"
}The /r/:code surface lets anonymous holders resolve and redeem voucher codes without an API key. GET /r/:code returns the current redemption state (used by the public lander to render the redeem page); POST /r/:code performs the redemption. POST /r/:code for an eSIM voucher returns 201 with { kind: "esim", installToken, installUrl } on synchronous success, or 202 with { kind: "processing", pollAfterMs } when eSIMfx accepted the order but the LPA isn't yet available. On 202 the traveller-facing lander polls GET /r/:code every pollAfterMs ms until the state settles; a second POST while the code is PROVISIONING returns 409 CONFLICT pointing at GET /r/:code. Terminal REDEEM_FAILED (worker exhausted attempts or hit INVALID_REFERENCE) collapses to an opaque 410 GONE — the same shape as REVOKED / EXPIRED / REFUNDED — so the internal redeem_fail_reason never leaks to the anonymous caller. GET /r/:code/usage returns remaining-data for a redeemed code, polled with a 60-second per-ICCID window and always-200 with a discriminated status field.
Access is IP-bucket and per-code-hash throttled (GET /r/:code: 10/min/IP + 30/min/code; POST /r/:code: 5/min/IP + 5/min/code; GET /r/:code/usage: 5/min/IP + 15/min/code) — never API-key authenticated. The :code path parameter is length-capped at 32 characters; error responses do not echo the raw caller-supplied code, and throttled responses are emitted before the service is consulted so brute-force enumeration stays bounded.
| code required | string |
{- "kind": "issued",
- "code": "string",
- "batchLabel": "string",
- "expiresAt": "string",
- "planSummary": {
- "countryCode": "string",
- "countryName": "string",
- "dataAmount": "string",
- "validityDays": 0
}, - "pollAfterMs": 0
}| code required | string |
| email required | string Traveller email where the install link will be delivered. VouchersService normalises this to lower-case + trimmed before persistence and idempotency comparison. |
| locale | string Enum: "en" "de" Locale for the install email + install page. Defaults to 'en'. 'de' falls back to en until Phase 2 fills the DE template (mirrors OriginationService default). |
{- "email": "string",
- "locale": "en"
}{- "kind": "esim",
- "installToken": "string",
- "installUrl": "string",
- "topupOrderId": "string",
- "dataAmount": "string",
- "dataAmountUnit": "string",
- "validityDays": 0,
- "pollAfterMs": 0
}| code required | string |
{- "status": "active",
- "plan": "string",
- "isUnlimited": true,
- "data": {
- "remainingMb": 0,
- "totalMb": 0
}, - "expiresAt": "string",
- "usageUpdatedAt": "string",
- "nextPollAfterMs": 0
}Refunds are asynchronous. POST /orders/:id/refund returns 202 Accepted with a refundRequestedAt timestamp; the wallet credit and the REFUNDED status flip commit atomically only after upstream termination succeeds. Poll GET /orders/:id until status reaches REFUNDED (success) or REFUND_FAILED (60-day window exceeded, or admin-forced fail).
The credited amount equals the order's planPrice — that is, salePrice − provisioningFee. The $0.50 provisioning fee is retained on NEW orders because the upstream does not refund ESIM_CARD on termination; TOPUP refunds credit the full salePrice since their provisioningFee is "0.00". Sandbox refunds skip real upstream termination and commit inline.
Accepts the refund and returns 202. The body reports REFUNDED if upstream termination completes inline within ~3s; otherwise it reports REFUND_PENDING and a background worker drives the commit. Poll GET /orders/:id until status becomes REFUNDED (success) or REFUND_FAILED (60-day window missed or admin-forced). Sandbox refunds skip real upstream termination.
| id required | string |
{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}Admin-only financial exports, scoped by calendar month. GET /reports/financial?month=YYYY-MM returns per-month revenue, profitability, and per-customer rollups — with optional sections filtering (revenue, profitability, customers) and format selection (json, csv, pdf). Every response carries net-of-upstream-refund fields (upstreamRefundedAmount, netUpstreamCost, netMargin) alongside the gross figures so you can reconcile true after-refund margin.
Authenticate with either the full ADMIN_TOKEN or the read-only ADMIN_REPORT_TOKEN as a bearer credential. Throttled at 5 reports/minute/IP because report generation pulls multi-month aggregates and PDF exports run pdf-lib synchronously.
| id required | string |
| status | string Enum: "PENDING" "IN_FLIGHT" "DELIVERED" "FAILED" Filter by delivery status. Omit to list across all statuses (newest-first). |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| cursor | string Opaque cursor returned as |
{- "items": [
- {
- "id": "string",
- "customerId": "string",
- "eventType": "string",
- "eventId": "string",
- "status": "PENDING",
- "attemptCount": 0,
- "nextRetryAt": "2019-08-24T14:15:22Z",
- "lastStatusCode": 0,
- "lastError": "string",
- "firstAttemptedAt": "2019-08-24T14:15:22Z",
- "deliveredAt": "2019-08-24T14:15:22Z",
- "createdAt": "2019-08-24T14:15:22Z"
}
], - "nextCursor": "string"
}| month required | string Example: month=2026-03 Month in YYYY-MM format |
| format | string Default: "json" Enum: "json" "csv" "pdf" Export format |
| sections | string Example: sections=revenue,profitability Comma-separated sections to include |
{- "month": "string",
- "generatedAt": "string",
- "revenue": {
- "grossRevenue": "string",
- "totalUpstreamCost": "string",
- "grossMargin": "string",
- "marginPercent": "string",
- "refundedAmount": "string",
- "refundCount": 0,
- "netRevenue": "string",
- "orderCounts": {
- "new": 0,
- "topup": 0
}, - "daily": [
- {
- "date": "string",
- "grossRevenue": "string",
- "upstreamCost": "string",
- "grossMargin": "string",
- "refundedAmount": "string",
- "refundCount": 0,
- "orders": 0,
- "upstreamRefundedAmount": "0.88",
- "netUpstreamCost": "0.50",
- "netMargin": "0.00"
}
], - "upstreamRefundedAmount": "1.76",
- "netUpstreamCost": "1.88",
- "netMargin": "1.62"
}, - "profitability": {
- "plans": [
- {
- "planId": "string",
- "planName": "string",
- "orderCount": 0,
- "refundCount": 0,
- "grossRevenue": "string",
- "refundedAmount": "string",
- "netRevenue": "string",
- "grossMargin": "string",
- "unitMargin": "string",
- "upstreamRefundedAmount": "1.76",
- "netUpstreamCost": "1.88",
- "netMargin": "1.62"
}
]
}, - "customers": {
- "customers": [
- {
- "customerId": "string",
- "customerName": "string",
- "totalSpent": "string",
- "orderCount": 0,
- "refundCount": 0,
- "refundedAmount": "string",
- "netRevenue": "string"
}
]
}
}Your prepaid wallet funds every purchase. GET /wallets/balance returns the current balance in USD as a decimal string (e.g. "150.00"). Purchases via POST /orders and POST /esims/:id/topup debit the wallet atomically at order creation; approved refunds credit it back once upstream termination completes. There is no self-serve top-up endpoint — wallet reloads are handled out of band during onboarding and via account manager.
| customerId required | string Customer ID to credit |
| amount required | string Amount to credit in decimal string format |
| referenceId required | string Idempotency key — prevents duplicate credits on retry |
{- "customerId": "string",
- "amount": "100.00",
- "referenceId": "01930000-0000-7000-8000-000000000001"
}{- "customerId": "string",
- "credited": "string",
- "referenceId": "string",
- "applied": true
}Wholesale customers can register an HTTPS endpoint to receive real-time eSIM lifecycle events. PUT /webhooks/endpoint mints a fresh signing secret, POSTs a synchronous webhook.test event under a 10-second timeout, and persists the URL only on 2xx — the plaintext secret is returned in the response body exactly once, so capture it immediately. Companion routes let you rotate the secret, read the current configuration, and delete the endpoint (which clears URL, secret, and circuit state).
Delivered events are signed with HMAC-SHA256 in Stripe format (X-Esimds-Signature: t=<sec>,v1=<hex>). We enforce HTTPS, block private/loopback/metadata hosts against SSRF, and DNS-re-check the hostname at delivery time. Retryable failures (5xx, 408, 429, network errors) follow a 1m → 5m → 30m → 2h → 6h → 24h retry curve; other 4xx responses are treated as terminal on the first attempt. Sustained failure cools off the endpoint and eventually auto-disables it (a fresh PUT /webhooks/endpoint from you is the only way to re-enable). A self-contained reference verifier is shipped at docs/wholesale-webhooks-verifier.ts.
{- "enabled": true,
- "verifiedAt": "2026-08-31T14:22:31.000Z",
- "lastDeliveryAt": "2026-08-31T15:07:12.000Z",
- "consecutiveFailures": 0
}Generates a fresh signing secret, POSTs a signed webhook.test event to the supplied URL synchronously, and persists only on 2xx. The plaintext secret is returned exactly once — store it out-of-band. Non-2xx / network error / SSRF-blocked URL → 422 (no persistence).
| url required | string HTTPS URL that will receive signed webhook deliveries. Must be publicly reachable — private/loopback/link-local addresses are rejected. |
{
}{- "secret": "whsec_dGVzdC1zZWNyZXQtdmFsdWUtZm9yLWRvY3M",
- "verifiedAt": "2026-08-31T14:22:31.000Z"
}Clears the registered URL, secret hash, and cool-off state. Idempotent — safe to call when no endpoint is registered.
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}Re-runs the handshake against the existing URL with a fresh secret and swaps the stored hash only on 2xx. The plaintext new secret is returned exactly once. The old secret remains valid until this call commits — in-flight deliveries signed under it verify normally on the caller side. 404 if no endpoint is registered or the endpoint has been auto-disabled.
{- "secret": "whsec_dGVzdC1zZWNyZXQtdmFsdWUtZm9yLWRvY3M",
- "verifiedAt": "2026-08-31T14:22:31.000Z"
}| id required | string |
| status | string Enum: "PENDING" "IN_FLIGHT" "DELIVERED" "FAILED" Filter by delivery status. Omit to list across all statuses (newest-first). |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| cursor | string Opaque cursor returned as |
{- "items": [
- {
- "id": "string",
- "customerId": "string",
- "eventType": "string",
- "eventId": "string",
- "status": "PENDING",
- "attemptCount": 0,
- "nextRetryAt": "2019-08-24T14:15:22Z",
- "lastStatusCode": 0,
- "lastError": "string",
- "firstAttemptedAt": "2019-08-24T14:15:22Z",
- "deliveredAt": "2019-08-24T14:15:22Z",
- "createdAt": "2019-08-24T14:15:22Z"
}
], - "nextCursor": "string"
}| id required | string |
| deliveryId required | string |
| reason | string <= 500 characters Optional operator note recorded on the audit trail. Truncated at 500 characters. |
{- "reason": "string"
}{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| id required | string |
| deliveryId required | string |
| reason | string <= 500 characters Optional operator note recorded on the audit trail. Truncated at 500 characters. |
{- "reason": "string"
}{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| name required | string Customer name |
| environment required | string Enum: "live" "sandbox" Customer environment |
| kind required | string Enum: "WHOLESALE" "VOUCHER" Commercial product the customer is onboarded under. Required and immutable. WHOLESALE: customer-keyed API places wallet-funded orders + activates / tops-up eSIMs. VOUCHER: admin mints code batches against this customer; end-travellers redeem at /r/:code. Pricing for both kinds resolves from customer_plan_prices.price; the kind governs which endpoints the customer can call and how invoice lines are shaped (no provisioning-fee split on voucher lines). |
| country required | string Buyer country as ISO 3166-1 alpha-2, uppercase. Required and IMMUTABLE after create. Drives the VAT/PDV rate applied at invoice generation (via country_vat_rates lookup); unlisted countries fall back to 0% + label "VAT" (matches Serbian izvoz-usluga export exemption). Common values: RS (Serbia), HR (Croatia), DE (Germany), US (United States). A relocated reseller must be created as a new customer to avoid retroactively changing the tax basis of historical invoices. |
| addressLine1 required | string Buyer street address, line 1. Required on the printed invoice. |
| addressLine2 | string Buyer street address, line 2 (apartment / suite / floor). |
| city required | string Buyer city. Required on the printed invoice. |
| postalCode required | string Buyer postal code. Required on the printed invoice. |
| taxId required | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). Required at create per invoice design §13.2 — Serbian law requires the buyer PIB on every valid račun, and we are B2B-only, so every legitimate customer has a tax identifier of some kind. If a buyer has no jurisdiction-level tax ID at all (rare edge case), pass a placeholder here and update the row before generating a real invoice. |
| registrationId | string Buyer business registration id (MB in Serbia; equivalent elsewhere). Optional. |
{- "name": "Acme Corp",
- "environment": "live",
- "kind": "WHOLESALE",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "apiKey": "string"
}[- {
- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}
]{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "apiKey": "string"
}Creates a new customer with kind = ENTERPRISE. Idempotent on (name, taxId): repeat requests with the same pair return the same row. ENTERPRISE customers authenticate via session cookies (/auth/* + /app/*) and hold no customer-keyed API key.
| name required | string Enterprise customer display name. |
| country required | string Buyer country as ISO 3166-1 alpha-2, uppercase. Required and IMMUTABLE after create. Drives the VAT/PDV rate applied at invoice generation (via country_vat_rates lookup). |
| addressLine1 required | string Buyer street address, line 1. Required on the printed invoice. |
| addressLine2 | string Buyer street address, line 2 (apartment / suite / floor). |
| city required | string Buyer city. Required on the printed invoice. |
| postalCode required | string Buyer postal code. Required on the printed invoice. |
| taxId required | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). Required per invoice design §13.2. |
| registrationId | string Buyer business registration id (MB in Serbia; equivalent elsewhere). Optional. |
| creditLimitMinor required | string Credit ceiling in minor units (e.g. EUR cents) as a non-negative decimal string. Represented as a string so callers preserve full precision on ceilings above 2^53 (same convention as the /app/me credit fields — see MeResponse for the encoding rationale). |
| currency required | string Value: "EUR" ISO 4217 currency of the credit line. Hardcoded to |
| environment | string Default: "live" Enum: "live" "sandbox" Customer environment — |
{- "name": "Acme Enterprise",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "creditLimitMinor": "100000",
- "currency": "EUR",
- "environment": "live"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Creates the initial portal user for a freshly-created ENTERPRISE customer, links it via customer_users, mints a better-auth setup-password verification token, and sends a welcome email with the token embedded in the link. Idempotent on (customer_id, email): repeat calls return the existing user without resending the email. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Returns 409 with body detail EMAIL_SUPPRESSED: ... if the target email is on the SES suppression list.
| id required | string Enterprise customer id (UUID v7). |
| email required | string Email address of the first admin user. Normalised to lowercase server-side before the customer_users + email_suppressions lookups. |
| name required | string Display name of the first admin user. |
{- "name": "Admin One"
}{- "userId": "018f2b5d-a5cd-71a8-9e0e-8d7f5b0f2c73",
- "name": "Admin One",
- "customerId": "string"
}Sets credit_limit_minor to the caller-supplied non-negative value and appends an audit_events row with action = customer.credit_limit_updated. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). No reciprocal invariant with credit_outstanding_minor — the ledger design permits lowering the ceiling below current outstanding.
| id required | string Enterprise customer id (UUID v7). |
| creditLimitMinor required | string New credit ceiling in minor units (e.g. EUR cents) as a non-negative integer decimal string. |
{- "creditLimitMinor": "250000"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Updates any subset of { name, country, addressLine1, addressLine2, city, postalCode, taxId, registrationId }. Every field is optional; undefined (omitted) leaves the column untouched. addressLine2 and registrationId accept explicit null to clear a previously-set value. Refuses creditLimitMinor (use the dedicated /credit-limit endpoint), currency (EUR-only today), kind, and environment (both immutable after create) via the whitelist validation pipe — unknown fields return 422. Writes an audit_events row with action = customer.identity_updated capturing only the changed fields (before/after). Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Slice C preamble P1.
| id required | string Enterprise customer id (UUID v7). |
| name | string Enterprise customer display name. |
| country | string Buyer country as ISO 3166-1 alpha-2, uppercase. Country changes affect the VAT/PDV rate applied on the NEXT invoice — historic invoices are not re-computed. |
| addressLine1 | string Buyer street address, line 1. |
| addressLine2 | object or null Buyer street address, line 2. Pass |
| city | string Buyer city. |
| postalCode | string Buyer postal code. |
| taxId | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). WARNING: the create endpoint's |
| registrationId | object or null Buyer business registration id (MB in Serbia; equivalent elsewhere). Pass |
{- "name": "eSIM Data Store Internal",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Wipes the customer row, every linked customer_users row, every users row that was ONLY linked to this customer (cascades to sessions + accounts via schema FKs), and every orphan verifications row for those users. All inside one transaction — a failure at any step rolls the whole delete back. Returns 204 on success. Refuses with 409 CUSTOMER_HAS_ORDERS if the customer has any order_groups OR orders rows, CUSTOMER_HAS_LEDGER for credit_line_ledger rows, or the generic CUSTOMER_HAS_DOWNSTREAM_ROWS (with the offending pg constraint name) for any other FK-referencing table we did not explicitly guard (customer_plan_prices, invoices, esims, voucherBatches, customerWallets, …) — partially-consumed credit lines and finance-relevant history carry legal + accounting consequences that must be resolved with finance BEFORE purging. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Slice C preamble P1.
| id required | string Enterprise customer id (UUID v7). |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}Writes an ADJUSTMENT row to credit_line_ledger and increments customers.credit_outstanding_minor by the signed deltaMinor (positive grows debt; negative shrinks it). Both writes atomic in one transaction with a FOR UPDATE lock on the customer row. Returns 422 for zero delta (no-op ledger rows are a caller bug). Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (SEC-37 no-reveal).
| id required | string Enterprise customer id (UUID v7). |
| deltaMinor required | string Signed decimal-string delta in minor units. |
| reason required | string Free-form operator context (between 5 and 500 chars). Persisted on the ledger row |
{- "deltaMinor": "-1500",
- "reason": "Compensation for reconciler drift #4321 — root cause: retry storm"
}{- "ledgerRowId": "string",
- "deltaMinor": "-1500",
- "newOutstandingMinor": "3900"
}Reruns the per-customer invoice-generation transaction for the supplied (periodStart, periodEnd) window. Ops uses this after a monthly cron failedCount > 0 tick, once the source-data drift (e.g. currency mismatch surfaced by the H1 fail-loud path) has been corrected. Idempotent — a second replay for a period whose invoice already exists returns outcome: 'skipped' without touching the sequence or writing a second invoice row. Emits an enterprise_invoice.admin_replay audit event on every invocation (including failed attempts, via try/finally) so ops has a forensic trail. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (SEC-37 no-reveal). Returns 422 on any of: calendar-invalid date (e.g. 2026-02-31), non-UTC-midnight, periodEnd <= periodStart, a period whose shape does not match first-of-month → first-of-next-month (codex iter-3 High-1 — keeps the replay idempotency key aligned with the natural monthly cron so admin + cron cannot double-bill overlapping activity windows), or a period that is NOT a closed prior month (periodEnd > start-of-current-month UTC) — replays for the current or future month are rejected because they would persist a partial invoice and cause the natural cron to later skip the same key (codex iter-4 High-1). Throttled to 10 per minute PER CUSTOMER — bucket keyed on the :id path param via a custom generateKey (codex iter-3 Medium-2) so IP rotation cannot storm a single customer.
| id required | string Enterprise customer id (UUID v7). |
| periodStart required | string Inclusive UTC-midnight start of the billing period. MUST be the first day of a month (e.g. |
| periodEnd required | string Exclusive UTC-midnight end of the billing period. MUST be the first day of the month immediately following |
{- "periodStart": "2026-07-01T00:00:00.000Z",
- "periodEnd": "2026-08-01T00:00:00.000Z"
}{- "outcome": "generated",
- "invoiceId": "01948a7d-3d5e-7a52-9c8a-42b8bcf28aae"
}Writes an INVOICE_PAYMENT credit-line-ledger row, decrements customers.credit_outstanding_minor by the customer-net subtotal (re-derived from invoice_line_items — VAT is pass-through to the tax authority and stays out of the credit line), and stamps invoices.paid_at. All three writes are atomic in one transaction. Returns 409 invoice already paid if paid_at is already set (idempotency guard — no duplicate ledger row on the 409 path). Returns 409 cannot mark-paid a non-positive-subtotal invoice for a refund-heavy period (credit-memo settlement is a separate flow, not yet implemented) or a zero-subtotal invoice (data-bug surface). Returns 404 for unknown invoice ids OR invoices whose parent customer is not kind = ENTERPRISE (SEC-37 no-reveal).
| id required | string Invoice id (UUID v7). |
| paidAt required | string When the payment was received (ISO 8601 date or date-time). Stamped verbatim onto |
| method required | string Short label for the settlement channel ( |
| reference required | string Bank / wire reference or tracking id for reconciliation. Max 200 chars. |
{- "paidAt": "2026-08-09T14:00:00Z",
- "method": "bank_transfer",
- "reference": "REF-2026-08-000123"
}{- "invoiceId": "string",
- "paidAt": "string",
- "ledgerRowId": "string"
}| customerId required | string Customer ID to credit |
| amount required | string Amount to credit in decimal string format |
| referenceId required | string Idempotency key — prevents duplicate credits on retry |
{- "customerId": "string",
- "amount": "100.00",
- "referenceId": "01930000-0000-7000-8000-000000000001"
}{- "customerId": "string",
- "credited": "string",
- "referenceId": "string",
- "applied": true
}[- {
- "id": "string",
- "esimfxProductId": "string",
- "esimfxImsiProfile": "string",
- "name": "string",
- "description": "string",
- "upstreamCost": "string",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US",
- "destination": "string",
- "compatibleTopupProductIds": [
- "string"
], - "disabled": true,
- "disabledReason": "MANUAL",
- "removedFromUpstream": true,
- "createdAt": "2019-08-24T14:15:22Z"
}
]{- "status": "idle",
- "startedAt": "2019-08-24T14:15:22Z",
- "finishedAt": "2019-08-24T14:15:22Z",
- "result": {
- "created": 0,
- "updated": 0,
- "failed": [
- "string"
], - "warnings": [
- "string"
]
}, - "error": { }
}| disabledReason required | string Enum: "MANUAL" "NEGATIVE_MARGIN" "DUPLICATE" Re-enable all currently-disabled plans whose |
{- "disabledReason": "MANUAL"
}{- "enabled": 0,
- "disabledReason": "MANUAL"
}| id required | string |
| disabled required | boolean Whether the plan is disabled |
{- "disabled": true
}{- "id": "string",
- "esimfxProductId": "string",
- "esimfxImsiProfile": "string",
- "name": "string",
- "description": "string",
- "upstreamCost": "string",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US",
- "destination": "string",
- "compatibleTopupProductIds": [
- "string"
], - "disabled": true,
- "disabledReason": "MANUAL",
- "removedFromUpstream": true,
- "createdAt": "2019-08-24T14:15:22Z"
}| customerId required | string Customer owning the eSIM. |
| planId required | string Plan to issue. |
| email required | string Recipient email for the install link. |
required | object (SourceRef) |
| locale | string Enum: "en" "de" Email + install-page locale. Defaults to 'en'. 'de' falls back to en until Phase 2 fills the DE template. |
| expiresInDays | number [ 1 .. 360 ] Install token lifetime in days. Defaults to 360. Capped at 360 by InstallTokenService.sign() to track the eSIMfx upstream activate_by window. |
| travelerName | string Optional first name surfaced in the email greeting. |
{- "customerId": "string",
- "planId": "string",
- "email": "string",
- "sourceRef": {
- "kind": "admin_order",
- "id": "string"
}, - "locale": "en",
- "expiresInDays": 1,
- "travelerName": "string"
}{- "esimId": "string",
- "installToken": "string",
- "installUrl": "string"
}Bypasses the upstream check and atomically credits the wallet + flips status to REFUNDED. Use when ops has confirmed out-of-band that the upstream subscription is terminated. The reason is recorded in the audit log.
| id required | string |
| reason required | string [ 1 .. 500 ] characters Non-empty rationale for the manual override; recorded in the audit trail. |
{- "reason": "eSIMfx support confirmed termination via ticket #12345"
}{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}Flips status to REFUND_FAILED without crediting the wallet. Use when the refund is irrecoverable (e.g. confirmed fraud, duplicate, or upstream never had the order). The reason is recorded in the audit log.
| id required | string |
| reason required | string [ 1 .. 500 ] characters Non-empty rationale for the manual override; recorded in the audit trail. |
{- "reason": "eSIMfx support confirmed termination via ticket #12345"
}{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}| customerId required | string Customer to invoice. |
| periodStart required | string Inclusive UTC-midnight start of the invoicing window. Bare-date ISO string ('YYYY-MM-DD'). Datetimes / timezones / calendar-invalid dates rejected. |
| periodEnd required | string Exclusive UTC-midnight end of the invoicing window. Bare-date ISO string ('YYYY-MM-DD'). |
| locale | string Enum: "en" "sr" PDF language. 'en' (English, default) or 'sr' (Serbian Latin script). Controls all label text, date format (ISO YYYY-MM-DD vs DD.MM.YYYY.), money decimal separator (period vs comma), and line-item description prefixes. Font (Noto Sans) is Unicode-safe for both. |
object Manual override of the USD-to-RSD exchange rate. Auto-fetches from kurs.resenje.org (NBS mirror) at invoice-generation time when omitted for RS-country buyers. When supplied it is used verbatim regardless of buyer country -- useful for audit replay, mirror-outage fallback, and adding an RSD conversion to a non-RS invoice on the operator's discretion. Provide all three subfields together. |
{- "customerId": "string",
- "periodStart": "2026-05-01",
- "periodEnd": "2026-06-01",
- "locale": "en",
- "exchangeRate": {
- "rateDate": "2026-07-16",
- "rateUsdToRsd": "102.3293",
- "source": "NBS_KURS"
}
}{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| customerId required | string |
[- {
- "planId": "string",
- "esimfxProductId": "string",
- "name": "string",
- "disabled": true,
- "disabledReason": { },
- "removedFromUpstream": true,
- "upstreamCost": "string",
- "price": "string",
- "priceDisabled": true,
- "priceDisabledReason": { },
- "zeroMarginAllowed": true
}
]| customerId required | string |
{ }{- "enabled": 0
}| customerId required | string Customer UUID who will be invoiced for each redemption. |
| planId required | string Plan UUID to issue against each redemption. |
| count required | number [ 1 .. 2000 ] Number of codes to generate. Hard cap 2,000 to keep the bundle in-memory; larger jobs must split or move to DO Spaces (deferred). |
| expiresAt | object Batch expiry (ISO 8601, UTC). |
| label required | string <= 120 characters Human-readable batch label. PUBLIC — shown to end-travellers in the /r/[code] redemption-page footer. Do NOT include PII, internal identifiers, or domain-shaped strings. Period ( |
| createdBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Persisted on voucher_batches.created_by AND echoed onto every audit_events row this batch produces. Must contain at least one non-whitespace character — a whitespace-only " " would otherwise satisfy |
| topupPlanId | string UUID of the companion TOPUP plan. When set, this batch is a BUNDLE: each code generates a paired TOPUP code (PENDING_BINDING) that becomes redeemable after the paired ESIM code is redeemed. Must be in the eSIM plan's |
{- "customerId": "string",
- "planId": "string",
- "count": 1,
- "expiresAt": { },
- "label": "VisaCo Croatia 2026-06",
- "createdBy": "string",
- "topupPlanId": "string"
}{- "batchId": "string",
- "codeCount": 0,
- "kind": "STANDARD",
- "topupPlanId": "string"
}| customerId | string Filter by customer UUID. Omit to list across all customers. |
| status | string Enum: "ACTIVE" "REVOKED" Filter by batch status. ACTIVE includes batches whose individual codes may have been revoked; REVOKED is set only when the WHOLE batch was revoked. |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "items": [
- {
- "batchId": "string",
- "customerId": "string",
- "planId": "string",
- "kind": "STANDARD",
- "topupPlanId": "string",
- "label": "string",
- "codeCount": 0,
- "expiresAt": { },
- "status": "ACTIVE",
- "createdBy": "string",
- "createdAt": "string"
}
], - "total": 0
}| id required | string |
{- "batch": {
- "batchId": "string",
- "customerId": "string",
- "planId": "string",
- "kind": "STANDARD",
- "topupPlanId": "string",
- "label": "string",
- "codeCount": 0,
- "expiresAt": { },
- "status": "ACTIVE",
- "createdBy": "string",
- "createdAt": "string"
}, - "stats": {
- "issued": 0,
- "redeeming": 0,
- "redeemed": 0,
- "revoked": 0,
- "expired": 0,
- "pendingBinding": 0,
- "refunded": 0,
- "provisioning": 0,
- "redeemFailed": 0
}
}| id required | string |
| status | string Enum: "ISSUED" "REDEEMING" "PROVISIONING" "REDEEMED" "REVOKED" "EXPIRED" "PENDING_BINDING" "REFUNDED" "REDEEM_FAILED" Filter by code status. Common operator paths: ?status=ISSUED to find a still-redeemable code for single-code revoke (backlog task #31 motivator), ?status=REDEEMED for invoice/audit triage. |
| pairKind | string Enum: "ESIM" "TOPUP" Filter by bundle half. ESIM = starter half, TOPUP = companion half. STANDARD-batch rows carry |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "items": [
- {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "pairKind": "ESIM",
- "pairId": "string",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "priceMinor": "string",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "createdAt": "2019-08-24T14:15:22Z"
}
], - "total": 0
}| id required | string |
| reason | object <= 500 characters Optional revoke reason. If null / empty / whitespace, |
| revokedBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Stored only on the audit_events row; voucher_codes itself has no |
{- "reason": { },
- "revokedBy": "string"
}{- "batchId": "string",
- "revokedCodeCount": 0,
- "failedCodeCount": 0
}| id required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| id required | string |
| reason | object <= 500 characters Optional revoke reason. If null / empty / whitespace, |
| revokedBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Stored only on the audit_events row; voucher_codes itself has no |
{- "reason": { },
- "revokedBy": "string"
}{- "codeId": "string"
}| pairId required | string |
{- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "esim": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topup": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}| codeId required | string |
{- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "esim": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topup": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}string Look up by traveller email (redeemed_email OR bound_email). The most-recent match is returned when an email appears on multiple codes. | |
| code | string Look up by human-displayable voucher code string. |
| codeId | string Look up by voucher_codes.id (UUIDv7). Accepted for programmatic clients; operators normally use |
| fresh | boolean Bypass the 60s upstream eSIMfx snapshot cache. Defaults to false. Set when triaging suspected upstream/local divergence. |
{- "voucherCode": {
- "id": "string",
- "code": "string",
- "status": "ISSUED",
- "pairKind": "string",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "refundedAt": "2019-08-24T14:15:22Z",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "esimfxOrderId": "string",
- "provisioningIccid": "string",
- "provisioningAttempts": 0,
- "nextRetryAt": "2019-08-24T14:15:22Z",
- "redeemFailedAt": "2019-08-24T14:15:22Z",
- "redeemFailReason": "string"
}, - "batch": {
- "id": "string",
- "label": "string",
- "kind": "STANDARD",
- "planId": "string",
- "topupPlanId": "string",
- "status": "string",
- "expiresAt": "2019-08-24T14:15:22Z"
}, - "esim": {
- "id": "string",
- "iccid": "string",
- "status": "string",
- "installTokenExpiresAt": "2019-08-24T14:15:22Z",
- "redeemedAt": "2019-08-24T14:15:22Z"
}, - "upstream": {
- "iccid": "string",
- "status": "string",
- "fetchedAt": "2019-08-24T14:15:22Z",
- "cached": true
}, - "upstreamOrder": {
- "orderId": "string",
- "orderStatus": "string",
- "subscription": {
- "status": "string",
- "activationTime": "string",
- "expiry": "string",
- "activateBy": "string",
- "upperLimitAmount": 0,
- "usedAmount": 0,
- "amountUnit": "string"
}, - "fetchedAt": "2019-08-24T14:15:22Z",
- "cached": true
}, - "esimHalf": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topupHalf": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}| code required | string Human-displayable voucher code string. Must be the ESIM-half on bundle codes — TOPUP-halves have no install link. |
string Override destination email. Defaults to the on-disk | |
| locale | string Render locale for the install email template. Defaults to the eSIM row's stored locale, else |
| reason required | string Free-form operator-provided reason (recorded in the audit event). Surfaced in support tooling so the support agent can see why an admin resent. |
| resentBy required | string Operator handle (admin email / system actor). Recorded in the |
{- "code": "string",
- "email": "string",
- "locale": "string",
- "reason": "string",
- "resentBy": "string"
}{- "installUrl": "string",
- "sentTo": "string",
- "emailDelivered": true
}| code required | string Voucher code string (ESIM-half, TOPUP-half, or standalone). |
| disputeId required | string Stripe / payment-processor dispute identifier. Free-form text — recorded in audit so support can correlate the chargeback ticket. |
| reason required | string Free-form chargeback reason (fraud, friendly-fraud, etc). Recorded verbatim in the audit row. |
| actor required | string Operator handle (admin email / system actor) — recorded as |
{- "code": "string",
- "disputeId": "string",
- "reason": "string",
- "actor": "string"
}{- "code": "string",
- "esimId": "string",
- "terminatedAt": "2019-08-24T14:15:22Z",
- "cascaded": true
}| code required | string Voucher code string. ESIM-half refund cascades to the paired TOPUP-half. |
| reason required | string Free-form refund reason (defective device, customer dispute, etc.). Recorded verbatim on voucher_codes.refund_reason + audit. |
| refundedBy required | string Operator handle (admin email / system actor). Recorded as refunded_by on the voucher_codes row + audit. |
| notifyTraveller required | boolean Send a "your voucher has been refunded" email to the on-disk redeemed_email (bundle cascade sends one email per pair). Required — the operator must make an explicit choice per call. Set |
{- "code": "string",
- "reason": "string",
- "refundedBy": "string",
- "notifyTraveller": true
}{- "code": "string",
- "refundedAt": "2019-08-24T14:15:22Z",
- "cascaded": true,
- "cascadeKind": "refund"
}| code required | string Voucher code string (ESIM-half or standalone). TOPUP-half codes are rejected with 422. |
| reason required | string Free-form reissue reason (profile fault, scan failure, etc). Recorded in audit. |
| reissuedBy required | string Operator handle (admin email / system actor). Recorded in audit. |
| locale | string Render locale for the install email template. Defaults to the eSIM row's stored locale, else |
string Destination override for the new install email. Defaults to the on-disk redeemed_email. Use when support previously corrected the address via resend-install-email and the reissue should NOT fall back to the original (stale) one. |
{- "code": "string",
- "reason": "string",
- "reissuedBy": "string",
- "locale": "string",
- "email": "string"
}{- "code": "string",
- "action": "swapped",
- "esimId": "string",
- "iccid": "string",
- "installUrl": "string",
- "sentTo": "string",
- "emailDelivered": true
}{- "items": [
- {
- "reason": "hard_bounce",
- "suppressedAt": "2026-07-11T04:29:26.870Z",
- "firstNotificationId": "265d09b0-2099-5fc5-9cbb-6b7cb1b33d0b"
}
], - "total": 43,
- "limit": 100,
- "offset": 0
}{ }{- "deleted": true
}{ }{- "summary": {
- "removed": 42,
- "skipped": 1
}
}{- "customer": {
- "id": "string",
- "name": "string",
- "kind": "ENTERPRISE",
- "currency": "EUR"
}, - "credit": {
- "limit": "100000",
- "outstanding": "0",
- "available": "100000"
}
}| limit | number [ 1 .. 100 ] Default: 20 Page size (1..100). Defaults to 20. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "rows": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000010",
- "invoiceNumber": "2026-00001",
- "periodStart": "2026-07-01T00:00:00.000Z",
- "periodEnd": "2026-08-01T00:00:00.000Z",
- "totalMinor": "5400",
- "currency": "EUR",
- "generatedAt": "2026-08-01T04:00:00.000Z"
}
]
}Renders and streams the invoice PDF. Returns 200 with application/pdf bytes and Content-Disposition: attachment. 404 for any invoice the caller does not own (SEC-37 non-existence-leak — the response body does not distinguish "not found" from "not owned").
| id required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}[- {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "name": "Japan 10GB / 7 days",
- "country": "JP",
- "duration": 7,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "isUnlimited": false,
- "price": "18.50",
- "currency": "EUR"
}
]| id required | string Plan id (UUID v7). |
{- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "name": "Japan 10GB / 7 days",
- "country": "JP",
- "duration": 7,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "isUnlimited": false,
- "price": "18.50",
- "currency": "EUR"
}{- "creditLimit": "100000",
- "outstanding": "25000",
- "available": "75000",
- "currency": "EUR",
- "recentDebits": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "createdAt": "2026-01-01T12:00:00.000Z",
- "kind": "ORDER_DEBIT",
- "label": "Order",
- "deltaMinor": "5000"
}
], - "recentCredits": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "createdAt": "2026-01-01T12:00:00.000Z",
- "kind": "ORDER_DEBIT",
- "label": "Order",
- "deltaMinor": "5000"
}
]
}| limit | number [ 1 .. 100 ] Default: 20 Page size (1..100). Defaults to 20. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
| from | string Example: from=2026-01-01T00:00:00Z Inclusive lower bound on |
| to | string Example: to=2026-01-31T23:59:59Z Inclusive upper bound on |
{- "rows": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "createdAt": "2026-01-01T12:00:00.000Z",
- "kind": "ORDER_DEBIT",
- "label": "Order",
- "deltaMinor": "5000"
}
], - "total": 42
}Debit the credit line for the cart total, insert one order_groups row + one orders row per unit, and post an ORDER_DEBIT ledger entry — all inside a single tx. Returns immediately with the created IDs; the receipt PDF (receiptPdfUrl) is filled by a post-commit hook (D10) and available via GET /app/order-groups/:id/receipt.pdf (D11).
| idempotency-key required | string |
required | Array of objects (CartLine) Cart lines. 1..50 entries. |
required | object Portal-side acknowledgments — both booleans MUST be true. |
{- "lines": [
- {
- "planId": "019ad4d5-8a3c-7000-8000-000000000001",
- "quantity": 2
}
], - "acknowledgments": {
- "deviceCompatibility": true,
- "terms": true
}
}{- "orderGroupId": "019ad4d5-8a3c-7000-8000-000000000010",
- "orderIds": [
- "019ad4d5-8a3c-7000-8000-000000000011",
- "019ad4d5-8a3c-7000-8000-000000000012"
], - "receiptPdfUrl": null
}| limit | number [ 1 .. 100 ] Default: 20 Page size (1..100). Defaults to 20. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
| from | string Example: from=2026-01-01T00:00:00Z Inclusive lower bound on |
| to | string Example: to=2026-01-31T23:59:59Z Inclusive upper bound on |
{- "rows": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T10:00:00Z",
- "totalMinor": "2100",
- "currency": "EUR",
- "receiptPdfUrl": null,
- "statusSummary": {
- "total": 2,
- "confirmed": 1,
- "pending": 1,
- "failed": 0,
- "refunded": 0,
- "refundPending": 0
}
}
], - "total": 42
}| id required | string |
{- "id": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T10:00:00Z",
- "totalMinor": "2100",
- "currency": "EUR",
- "receiptPdfUrl": null,
- "statusSummary": {
- "total": 2,
- "confirmed": 1,
- "pending": 1,
- "failed": 0,
- "refunded": 0,
- "refundPending": 0
}, - "lineItems": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000011",
- "status": "CONFIRM_PENDING",
- "planId": "019ad4d5-8a3c-7000-8000-000000000005",
- "planName": "Turkey_1GB_7DAYs",
- "salePrice": "10.50",
- "provisioningFee": "0.50",
- "iccid": null,
- "createdAt": "2026-08-06T10:00:00Z"
}
]
}Renders and streams the receipt PDF for the given order group. Returns 200 with application/pdf bytes. 404 for any group the caller does not own (SEC-37 non-existence-leak — the response body does not distinguish "not found" from "not owned"). A group with zero committed orders (all CONFIRM_PENDING / CONFIRM_FAILED) also 404s until at least one order commits.
| id required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| id required | string |
{- "id": "019ad4d5-8a3c-7000-8000-000000000011",
- "status": "CONFIRM_PENDING",
- "planId": "019ad4d5-8a3c-7000-8000-000000000005",
- "planName": "Turkey_1GB_7DAYs",
- "salePrice": "10.50",
- "provisioningFee": "0.50",
- "iccid": null,
- "createdAt": "2026-08-06T10:00:00Z",
- "refundRequestedAt": "2026-08-09T12:00:00Z",
- "esim": {
- "iccid": "8944000000000000001",
- "esimQr": "LPA:1$rsp.example.com$FOO"
}
}Accepts the refund and returns 202. The body reports REFUNDED if upstream termination completes inline within ~3s; otherwise REFUND_PENDING and the background worker drives the commit. Poll GET /app/orders/:id until status is REFUNDED or REFUND_FAILED. Refund credit posts to the credit-line ledger as REFUND_CREDIT with delta = -planPrice (design §3.6 — provisioning fee retained per FIN-32).
| id required | string |
{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}Returns a paginated list of the caller's eSIMs plus a filter-independent KPI summary. Filters compose against the customer-scoped base via AND at the service (see design §5.5).
| status | string Enum: "PROVISIONED" "ACTIVE" "EXPIRED" "BLOCKED" Filter by |
| assigned | boolean Example: assigned=false Tri-state filter on assignment. |
| iccidPrefix | string <= 20 characters Example: iccidPrefix=891234 ICCID prefix search (LIKE ' |
| limit | number [ 1 .. 100 ] Default: 20 Page size (1..100). Defaults to 20. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "items": [
- {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "iccid": "8944500000000000001",
- "status": "PROVISIONED",
- "planName": "Turkey_1GB_7DAYs",
- "assignedAt": "2026-08-06T10:00:00Z",
- "redeemedAt": "2026-08-06T12:00:00Z",
- "installTokenExpiresAt": "2026-09-05T10:00:00Z"
}
], - "summary": {
- "total": 42,
- "unassigned": 5,
- "assigned": 10,
- "installed": 20,
- "expired": 5,
- "revoked": 2
}, - "total": 42
}Returns the caller's eSIM row plus its plan back-ref, order back-ref (via source_ref_kind='ENTERPRISE_ORDER'), user-facing status timeline (whitelisted actions only, ASC by createdAt), and usage summary. Cross-customer ids and non-existent ids both return an identical 404 payload (SEC-37 non-existence-leak).
| id required | string |
{- "esim": {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "iccid": "8944500000000000001",
- "status": "PROVISIONED",
- "assignedAt": "2026-08-06T10:00:00Z",
- "redeemedAt": "2026-08-06T12:00:00Z",
- "installTokenExpiresAt": "2026-09-05T10:00:00Z",
- "planId": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T09:00:00Z",
- "updatedAt": "2026-08-06T12:00:00Z"
}, - "plan": {
- "id": "019ad4d5-8a3c-7000-8000-000000000010",
- "name": "Turkey_1GB_7DAYs"
}, - "order": {
- "id": "019ad4d5-8a3c-7000-8000-000000000020"
}, - "timeline": [
- {
- "action": "esim.issued",
- "createdAt": "2026-08-06T09:00:00Z",
- "actorUserId": "019ad4d5-8a3c-7000-8000-000000000030",
- "detail": null
}
], - "usage": {
- "amountBytes": 1234567890,
- "limitBytes": 10737418240,
- "updatedAt": "2026-08-06T13:00:00Z",
- "serviceExpiresAt": "2026-09-05T09:00:00Z",
- "serviceActivatedAt": "2026-08-06T12:00:00Z",
- "nextPollAfterMs": 60000
}
}Row-locks the target eSIM, stamps traveller_email + assigned_at + assigned_by_user_id, mints a fresh install token, and dispatches the NOTIF-1 CID-inline install email best-effort. Returns 200 with the refreshed eSIM row + installUrl. Send Idempotency-Key to guarantee at-most-once semantics on retries (design §4.7).
| id required | string |
| travellerEmail required | string <= 254 characters Traveller email that will receive the install email and be recorded on |
{- "travellerEmail": "[email protected]"
}{- "esim": {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "iccid": "8944500000000000001",
- "status": "PROVISIONED",
- "assignedAt": "2026-08-06T10:00:00Z",
- "redeemedAt": "2026-08-06T12:00:00Z",
- "installTokenExpiresAt": "2026-09-05T10:00:00Z",
- "planId": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T09:00:00Z",
- "updatedAt": "2026-08-06T12:00:00Z"
}, - "installTokenExpiresAt": "2027-08-02T10:00:00Z"
}Row-locks the target eSIM, then atomically (single tx): overwrites traveller_email + assigned_at + assigned_by_user_id AND bumps install_token_version AND mints the fresh install-token JWT signed against the bumped version. The previous traveller's install URL falls to 410 GONE atomically at reassign commit — there is NO window where BOTH old and new URLs resolve. Concurrent reassign races serialise on the row lock. Post-commit (best-effort, never blocks the 200): dispatches the NOTIF-1 CID-inline install email, emits an esim.reassigned audit event with the old and new emails. Returns 200 with the refreshed eSIM row + installUrl. Send Idempotency-Key to guarantee at-most-once semantics on retries — the atomicity guarantee holds within a single request and the idempotency cache short-circuits replays before any DB write (design §4.4, §4.7).
| id required | string |
| travellerEmail required | string <= 254 characters New traveller email that will overwrite |
{- "travellerEmail": "[email protected]"
}{- "esim": {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "iccid": "8944500000000000001",
- "status": "PROVISIONED",
- "assignedAt": "2026-08-06T10:00:00Z",
- "redeemedAt": "2026-08-06T12:00:00Z",
- "installTokenExpiresAt": "2026-09-05T10:00:00Z",
- "planId": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T09:00:00Z",
- "updatedAt": "2026-08-06T12:00:00Z"
}, - "installTokenExpiresAt": "2027-08-02T10:00:00Z"
}Re-sends the install email to the SAME travellerEmail currently stamped on the row (no body — callers cannot override the recipient). Refreshes installTokenExpiresAt when the current value is null / past / within 7 days from now (design §4.5 near-expiry threshold); preserves it otherwise. Does NOT bump install_token_version — the traveller's ORIGINAL install URL (and every prior resend URL) remain valid. Rate-limited to 3 sends per rolling 24h per esim_id — a 4th call inside the window returns 429 with a Retry-After header (delta-seconds) computed off the oldest prior send. Idempotency-key replay returns the cached response and does NOT double-send the email or double-count against the rate limit.
| id required | string |
{- "esim": {
- "id": "019ad4d5-8a3c-7000-8000-000000000001",
- "iccid": "8944500000000000001",
- "status": "PROVISIONED",
- "assignedAt": "2026-08-06T10:00:00Z",
- "redeemedAt": "2026-08-06T12:00:00Z",
- "installTokenExpiresAt": "2026-09-05T10:00:00Z",
- "planId": "019ad4d5-8a3c-7000-8000-000000000010",
- "createdAt": "2026-08-06T09:00:00Z",
- "updatedAt": "2026-08-06T12:00:00Z"
}, - "installTokenExpiresAt": "2027-08-02T10:00:00Z"
}Flips the parent order to REFUND_PENDING inside a single row-locked tx and emits an esim.revoked audit event; the refund cron (advisory lock 100006) drives the subsequent terminate_subscription upstream call, the eSIM's PROVISIONED → BLOCKED transition, and the credit-line refund asynchronously. Returns 202 with refundStatusUrl pointing at /app/orders/:orderId for polling. Allowed while esim.status IN ('PROVISIONED', 'ACTIVE') AND the parent order is in COMPLETED; concurrent revokes on the same eSIM serialise on the row lock (the loser returns 409 NOT_PROVISIONED). Send Idempotency-Key to guarantee at-most-once semantics on retries — same-key replay returns the cached 202 without re-writing the order. Design §4.6.
| id required | string |
{- "refundStatusUrl": "/app/orders/019ad4d5-8a3c-7000-8000-000000000001",
- "orderStatus": "REFUND_PENDING"
}| name required | string Customer name |
| environment required | string Enum: "live" "sandbox" Customer environment |
| kind required | string Enum: "WHOLESALE" "VOUCHER" Commercial product the customer is onboarded under. Required and immutable. WHOLESALE: customer-keyed API places wallet-funded orders + activates / tops-up eSIMs. VOUCHER: admin mints code batches against this customer; end-travellers redeem at /r/:code. Pricing for both kinds resolves from customer_plan_prices.price; the kind governs which endpoints the customer can call and how invoice lines are shaped (no provisioning-fee split on voucher lines). |
| country required | string Buyer country as ISO 3166-1 alpha-2, uppercase. Required and IMMUTABLE after create. Drives the VAT/PDV rate applied at invoice generation (via country_vat_rates lookup); unlisted countries fall back to 0% + label "VAT" (matches Serbian izvoz-usluga export exemption). Common values: RS (Serbia), HR (Croatia), DE (Germany), US (United States). A relocated reseller must be created as a new customer to avoid retroactively changing the tax basis of historical invoices. |
| addressLine1 required | string Buyer street address, line 1. Required on the printed invoice. |
| addressLine2 | string Buyer street address, line 2 (apartment / suite / floor). |
| city required | string Buyer city. Required on the printed invoice. |
| postalCode required | string Buyer postal code. Required on the printed invoice. |
| taxId required | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). Required at create per invoice design §13.2 — Serbian law requires the buyer PIB on every valid račun, and we are B2B-only, so every legitimate customer has a tax identifier of some kind. If a buyer has no jurisdiction-level tax ID at all (rare edge case), pass a placeholder here and update the row before generating a real invoice. |
| registrationId | string Buyer business registration id (MB in Serbia; equivalent elsewhere). Optional. |
{- "name": "Acme Corp",
- "environment": "live",
- "kind": "WHOLESALE",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "apiKey": "string"
}[- {
- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}
]{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "kind": "WHOLESALE",
- "balance": "150.00",
- "createdAt": "2019-08-24T14:15:22Z",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "apiKey": "string"
}Creates a new customer with kind = ENTERPRISE. Idempotent on (name, taxId): repeat requests with the same pair return the same row. ENTERPRISE customers authenticate via session cookies (/auth/* + /app/*) and hold no customer-keyed API key.
| name required | string Enterprise customer display name. |
| country required | string Buyer country as ISO 3166-1 alpha-2, uppercase. Required and IMMUTABLE after create. Drives the VAT/PDV rate applied at invoice generation (via country_vat_rates lookup). |
| addressLine1 required | string Buyer street address, line 1. Required on the printed invoice. |
| addressLine2 | string Buyer street address, line 2 (apartment / suite / floor). |
| city required | string Buyer city. Required on the printed invoice. |
| postalCode required | string Buyer postal code. Required on the printed invoice. |
| taxId required | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). Required per invoice design §13.2. |
| registrationId | string Buyer business registration id (MB in Serbia; equivalent elsewhere). Optional. |
| creditLimitMinor required | string Credit ceiling in minor units (e.g. EUR cents) as a non-negative decimal string. Represented as a string so callers preserve full precision on ceilings above 2^53 (same convention as the /app/me credit fields — see MeResponse for the encoding rationale). |
| currency required | string Value: "EUR" ISO 4217 currency of the credit line. Hardcoded to |
| environment | string Default: "live" Enum: "live" "sandbox" Customer environment — |
{- "name": "Acme Enterprise",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "creditLimitMinor": "100000",
- "currency": "EUR",
- "environment": "live"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Creates the initial portal user for a freshly-created ENTERPRISE customer, links it via customer_users, mints a better-auth setup-password verification token, and sends a welcome email with the token embedded in the link. Idempotent on (customer_id, email): repeat calls return the existing user without resending the email. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Returns 409 with body detail EMAIL_SUPPRESSED: ... if the target email is on the SES suppression list.
| id required | string Enterprise customer id (UUID v7). |
| email required | string Email address of the first admin user. Normalised to lowercase server-side before the customer_users + email_suppressions lookups. |
| name required | string Display name of the first admin user. |
{- "name": "Admin One"
}{- "userId": "018f2b5d-a5cd-71a8-9e0e-8d7f5b0f2c73",
- "name": "Admin One",
- "customerId": "string"
}Sets credit_limit_minor to the caller-supplied non-negative value and appends an audit_events row with action = customer.credit_limit_updated. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). No reciprocal invariant with credit_outstanding_minor — the ledger design permits lowering the ceiling below current outstanding.
| id required | string Enterprise customer id (UUID v7). |
| creditLimitMinor required | string New credit ceiling in minor units (e.g. EUR cents) as a non-negative integer decimal string. |
{- "creditLimitMinor": "250000"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Updates any subset of { name, country, addressLine1, addressLine2, city, postalCode, taxId, registrationId }. Every field is optional; undefined (omitted) leaves the column untouched. addressLine2 and registrationId accept explicit null to clear a previously-set value. Refuses creditLimitMinor (use the dedicated /credit-limit endpoint), currency (EUR-only today), kind, and environment (both immutable after create) via the whitelist validation pipe — unknown fields return 422. Writes an audit_events row with action = customer.identity_updated capturing only the changed fields (before/after). Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Slice C preamble P1.
| id required | string Enterprise customer id (UUID v7). |
| name | string Enterprise customer display name. |
| country | string Buyer country as ISO 3166-1 alpha-2, uppercase. Country changes affect the VAT/PDV rate applied on the NEXT invoice — historic invoices are not re-computed. |
| addressLine1 | string Buyer street address, line 1. |
| addressLine2 | object or null Buyer street address, line 2. Pass |
| city | string Buyer city. |
| postalCode | string Buyer postal code. |
| taxId | string Buyer tax ID (PIB for Serbia, VAT number for EU, etc.). WARNING: the create endpoint's |
| registrationId | object or null Buyer business registration id (MB in Serbia; equivalent elsewhere). Pass |
{- "name": "eSIM Data Store Internal",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx"
}{- "id": "string",
- "name": "string",
- "environment": "live",
- "country": "RS",
- "addressLine1": "Kneza Miloša 12",
- "addressLine2": "Sprat 4",
- "city": "Beograd",
- "postalCode": "11000",
- "taxId": "108xxxxxx",
- "registrationId": "21xxxxxx",
- "kind": "ENTERPRISE",
- "currency": "EUR",
- "creditLimitMinor": "100000",
- "creditOutstandingMinor": "0",
- "createdAt": "string",
- "updatedAt": "string"
}Wipes the customer row, every linked customer_users row, every users row that was ONLY linked to this customer (cascades to sessions + accounts via schema FKs), and every orphan verifications row for those users. All inside one transaction — a failure at any step rolls the whole delete back. Returns 204 on success. Refuses with 409 CUSTOMER_HAS_ORDERS if the customer has any order_groups OR orders rows, CUSTOMER_HAS_LEDGER for credit_line_ledger rows, or the generic CUSTOMER_HAS_DOWNSTREAM_ROWS (with the offending pg constraint name) for any other FK-referencing table we did not explicitly guard (customer_plan_prices, invoices, esims, voucherBatches, customerWallets, …) — partially-consumed credit lines and finance-relevant history carry legal + accounting consequences that must be resolved with finance BEFORE purging. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (no reveal). Slice C preamble P1.
| id required | string Enterprise customer id (UUID v7). |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}Writes an ADJUSTMENT row to credit_line_ledger and increments customers.credit_outstanding_minor by the signed deltaMinor (positive grows debt; negative shrinks it). Both writes atomic in one transaction with a FOR UPDATE lock on the customer row. Returns 422 for zero delta (no-op ledger rows are a caller bug). Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (SEC-37 no-reveal).
| id required | string Enterprise customer id (UUID v7). |
| deltaMinor required | string Signed decimal-string delta in minor units. |
| reason required | string Free-form operator context (between 5 and 500 chars). Persisted on the ledger row |
{- "deltaMinor": "-1500",
- "reason": "Compensation for reconciler drift #4321 — root cause: retry storm"
}{- "ledgerRowId": "string",
- "deltaMinor": "-1500",
- "newOutstandingMinor": "3900"
}Reruns the per-customer invoice-generation transaction for the supplied (periodStart, periodEnd) window. Ops uses this after a monthly cron failedCount > 0 tick, once the source-data drift (e.g. currency mismatch surfaced by the H1 fail-loud path) has been corrected. Idempotent — a second replay for a period whose invoice already exists returns outcome: 'skipped' without touching the sequence or writing a second invoice row. Emits an enterprise_invoice.admin_replay audit event on every invocation (including failed attempts, via try/finally) so ops has a forensic trail. Returns 404 if the customer does not exist OR is not kind = ENTERPRISE (SEC-37 no-reveal). Returns 422 on any of: calendar-invalid date (e.g. 2026-02-31), non-UTC-midnight, periodEnd <= periodStart, a period whose shape does not match first-of-month → first-of-next-month (codex iter-3 High-1 — keeps the replay idempotency key aligned with the natural monthly cron so admin + cron cannot double-bill overlapping activity windows), or a period that is NOT a closed prior month (periodEnd > start-of-current-month UTC) — replays for the current or future month are rejected because they would persist a partial invoice and cause the natural cron to later skip the same key (codex iter-4 High-1). Throttled to 10 per minute PER CUSTOMER — bucket keyed on the :id path param via a custom generateKey (codex iter-3 Medium-2) so IP rotation cannot storm a single customer.
| id required | string Enterprise customer id (UUID v7). |
| periodStart required | string Inclusive UTC-midnight start of the billing period. MUST be the first day of a month (e.g. |
| periodEnd required | string Exclusive UTC-midnight end of the billing period. MUST be the first day of the month immediately following |
{- "periodStart": "2026-07-01T00:00:00.000Z",
- "periodEnd": "2026-08-01T00:00:00.000Z"
}{- "outcome": "generated",
- "invoiceId": "01948a7d-3d5e-7a52-9c8a-42b8bcf28aae"
}Writes an INVOICE_PAYMENT credit-line-ledger row, decrements customers.credit_outstanding_minor by the customer-net subtotal (re-derived from invoice_line_items — VAT is pass-through to the tax authority and stays out of the credit line), and stamps invoices.paid_at. All three writes are atomic in one transaction. Returns 409 invoice already paid if paid_at is already set (idempotency guard — no duplicate ledger row on the 409 path). Returns 409 cannot mark-paid a non-positive-subtotal invoice for a refund-heavy period (credit-memo settlement is a separate flow, not yet implemented) or a zero-subtotal invoice (data-bug surface). Returns 404 for unknown invoice ids OR invoices whose parent customer is not kind = ENTERPRISE (SEC-37 no-reveal).
| id required | string Invoice id (UUID v7). |
| paidAt required | string When the payment was received (ISO 8601 date or date-time). Stamped verbatim onto |
| method required | string Short label for the settlement channel ( |
| reference required | string Bank / wire reference or tracking id for reconciliation. Max 200 chars. |
{- "paidAt": "2026-08-09T14:00:00Z",
- "method": "bank_transfer",
- "reference": "REF-2026-08-000123"
}{- "invoiceId": "string",
- "paidAt": "string",
- "ledgerRowId": "string"
}| customerId required | string Customer owning the eSIM. |
| planId required | string Plan to issue. |
| email required | string Recipient email for the install link. |
required | object (SourceRef) |
| locale | string Enum: "en" "de" Email + install-page locale. Defaults to 'en'. 'de' falls back to en until Phase 2 fills the DE template. |
| expiresInDays | number [ 1 .. 360 ] Install token lifetime in days. Defaults to 360. Capped at 360 by InstallTokenService.sign() to track the eSIMfx upstream activate_by window. |
| travelerName | string Optional first name surfaced in the email greeting. |
{- "customerId": "string",
- "planId": "string",
- "email": "string",
- "sourceRef": {
- "kind": "admin_order",
- "id": "string"
}, - "locale": "en",
- "expiresInDays": 1,
- "travelerName": "string"
}{- "esimId": "string",
- "installToken": "string",
- "installUrl": "string"
}Bypasses the upstream check and atomically credits the wallet + flips status to REFUNDED. Use when ops has confirmed out-of-band that the upstream subscription is terminated. The reason is recorded in the audit log.
| id required | string |
| reason required | string [ 1 .. 500 ] characters Non-empty rationale for the manual override; recorded in the audit trail. |
{- "reason": "eSIMfx support confirmed termination via ticket #12345"
}{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}Flips status to REFUND_FAILED without crediting the wallet. Use when the refund is irrecoverable (e.g. confirmed fraud, duplicate, or upstream never had the order). The reason is recorded in the audit log.
| id required | string |
| reason required | string [ 1 .. 500 ] characters Non-empty rationale for the manual override; recorded in the audit trail. |
{- "reason": "eSIMfx support confirmed termination via ticket #12345"
}{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50"
}| customerId required | string Customer to invoice. |
| periodStart required | string Inclusive UTC-midnight start of the invoicing window. Bare-date ISO string ('YYYY-MM-DD'). Datetimes / timezones / calendar-invalid dates rejected. |
| periodEnd required | string Exclusive UTC-midnight end of the invoicing window. Bare-date ISO string ('YYYY-MM-DD'). |
| locale | string Enum: "en" "sr" PDF language. 'en' (English, default) or 'sr' (Serbian Latin script). Controls all label text, date format (ISO YYYY-MM-DD vs DD.MM.YYYY.), money decimal separator (period vs comma), and line-item description prefixes. Font (Noto Sans) is Unicode-safe for both. |
object Manual override of the USD-to-RSD exchange rate. Auto-fetches from kurs.resenje.org (NBS mirror) at invoice-generation time when omitted for RS-country buyers. When supplied it is used verbatim regardless of buyer country -- useful for audit replay, mirror-outage fallback, and adding an RSD conversion to a non-RS invoice on the operator's discretion. Provide all three subfields together. |
{- "customerId": "string",
- "periodStart": "2026-05-01",
- "periodEnd": "2026-06-01",
- "locale": "en",
- "exchangeRate": {
- "rateDate": "2026-07-16",
- "rateUsdToRsd": "102.3293",
- "source": "NBS_KURS"
}
}{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| month required | string Example: month=2026-03 Month in YYYY-MM format |
| format | string Default: "json" Enum: "json" "csv" "pdf" Export format |
| sections | string Example: sections=revenue,profitability Comma-separated sections to include |
{- "month": "string",
- "generatedAt": "string",
- "revenue": {
- "grossRevenue": "string",
- "totalUpstreamCost": "string",
- "grossMargin": "string",
- "marginPercent": "string",
- "refundedAmount": "string",
- "refundCount": 0,
- "netRevenue": "string",
- "orderCounts": {
- "new": 0,
- "topup": 0
}, - "daily": [
- {
- "date": "string",
- "grossRevenue": "string",
- "upstreamCost": "string",
- "grossMargin": "string",
- "refundedAmount": "string",
- "refundCount": 0,
- "orders": 0,
- "upstreamRefundedAmount": "0.88",
- "netUpstreamCost": "0.50",
- "netMargin": "0.00"
}
], - "upstreamRefundedAmount": "1.76",
- "netUpstreamCost": "1.88",
- "netMargin": "1.62"
}, - "profitability": {
- "plans": [
- {
- "planId": "string",
- "planName": "string",
- "orderCount": 0,
- "refundCount": 0,
- "grossRevenue": "string",
- "refundedAmount": "string",
- "netRevenue": "string",
- "grossMargin": "string",
- "unitMargin": "string",
- "upstreamRefundedAmount": "1.76",
- "netUpstreamCost": "1.88",
- "netMargin": "1.62"
}
]
}, - "customers": {
- "customers": [
- {
- "customerId": "string",
- "customerName": "string",
- "totalSpent": "string",
- "orderCount": 0,
- "refundCount": 0,
- "refundedAmount": "string",
- "netRevenue": "string"
}
]
}
}| customerId required | string Customer UUID who will be invoiced for each redemption. |
| planId required | string Plan UUID to issue against each redemption. |
| count required | number [ 1 .. 2000 ] Number of codes to generate. Hard cap 2,000 to keep the bundle in-memory; larger jobs must split or move to DO Spaces (deferred). |
| expiresAt | object Batch expiry (ISO 8601, UTC). |
| label required | string <= 120 characters Human-readable batch label. PUBLIC — shown to end-travellers in the /r/[code] redemption-page footer. Do NOT include PII, internal identifiers, or domain-shaped strings. Period ( |
| createdBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Persisted on voucher_batches.created_by AND echoed onto every audit_events row this batch produces. Must contain at least one non-whitespace character — a whitespace-only " " would otherwise satisfy |
| topupPlanId | string UUID of the companion TOPUP plan. When set, this batch is a BUNDLE: each code generates a paired TOPUP code (PENDING_BINDING) that becomes redeemable after the paired ESIM code is redeemed. Must be in the eSIM plan's |
{- "customerId": "string",
- "planId": "string",
- "count": 1,
- "expiresAt": { },
- "label": "VisaCo Croatia 2026-06",
- "createdBy": "string",
- "topupPlanId": "string"
}{- "batchId": "string",
- "codeCount": 0,
- "kind": "STANDARD",
- "topupPlanId": "string"
}| customerId | string Filter by customer UUID. Omit to list across all customers. |
| status | string Enum: "ACTIVE" "REVOKED" Filter by batch status. ACTIVE includes batches whose individual codes may have been revoked; REVOKED is set only when the WHOLE batch was revoked. |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "items": [
- {
- "batchId": "string",
- "customerId": "string",
- "planId": "string",
- "kind": "STANDARD",
- "topupPlanId": "string",
- "label": "string",
- "codeCount": 0,
- "expiresAt": { },
- "status": "ACTIVE",
- "createdBy": "string",
- "createdAt": "string"
}
], - "total": 0
}| id required | string |
{- "batch": {
- "batchId": "string",
- "customerId": "string",
- "planId": "string",
- "kind": "STANDARD",
- "topupPlanId": "string",
- "label": "string",
- "codeCount": 0,
- "expiresAt": { },
- "status": "ACTIVE",
- "createdBy": "string",
- "createdAt": "string"
}, - "stats": {
- "issued": 0,
- "redeeming": 0,
- "redeemed": 0,
- "revoked": 0,
- "expired": 0,
- "pendingBinding": 0,
- "refunded": 0,
- "provisioning": 0,
- "redeemFailed": 0
}
}| id required | string |
| status | string Enum: "ISSUED" "REDEEMING" "PROVISIONING" "REDEEMED" "REVOKED" "EXPIRED" "PENDING_BINDING" "REFUNDED" "REDEEM_FAILED" Filter by code status. Common operator paths: ?status=ISSUED to find a still-redeemable code for single-code revoke (backlog task #31 motivator), ?status=REDEEMED for invoice/audit triage. |
| pairKind | string Enum: "ESIM" "TOPUP" Filter by bundle half. ESIM = starter half, TOPUP = companion half. STANDARD-batch rows carry |
| limit | number [ 1 .. 200 ] Default: 50 Page size (1-200). Defaults to 50. |
| offset | number >= 0 Default: 0 Row offset (>=0). Defaults to 0. |
{- "items": [
- {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "pairKind": "ESIM",
- "pairId": "string",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "priceMinor": "string",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "createdAt": "2019-08-24T14:15:22Z"
}
], - "total": 0
}| id required | string |
| reason | object <= 500 characters Optional revoke reason. If null / empty / whitespace, |
| revokedBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Stored only on the audit_events row; voucher_codes itself has no |
{- "reason": { },
- "revokedBy": "string"
}{- "batchId": "string",
- "revokedCodeCount": 0,
- "failedCodeCount": 0
}| id required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}| id required | string |
| reason | object <= 500 characters Optional revoke reason. If null / empty / whitespace, |
| revokedBy required | string <= 120 characters Operator identifier (email or name) for the audit trail. REQUIRED. Stored only on the audit_events row; voucher_codes itself has no |
{- "reason": { },
- "revokedBy": "string"
}{- "codeId": "string"
}| pairId required | string |
{- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "esim": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topup": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}| codeId required | string |
{- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "esim": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topup": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}string Look up by traveller email (redeemed_email OR bound_email). The most-recent match is returned when an email appears on multiple codes. | |
| code | string Look up by human-displayable voucher code string. |
| codeId | string Look up by voucher_codes.id (UUIDv7). Accepted for programmatic clients; operators normally use |
| fresh | boolean Bypass the 60s upstream eSIMfx snapshot cache. Defaults to false. Set when triaging suspected upstream/local divergence. |
{- "voucherCode": {
- "id": "string",
- "code": "string",
- "status": "ISSUED",
- "pairKind": "string",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "pairId": "string",
- "batchId": "string",
- "customerId": "string",
- "refundedAt": "2019-08-24T14:15:22Z",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "esimfxOrderId": "string",
- "provisioningIccid": "string",
- "provisioningAttempts": 0,
- "nextRetryAt": "2019-08-24T14:15:22Z",
- "redeemFailedAt": "2019-08-24T14:15:22Z",
- "redeemFailReason": "string"
}, - "batch": {
- "id": "string",
- "label": "string",
- "kind": "STANDARD",
- "planId": "string",
- "topupPlanId": "string",
- "status": "string",
- "expiresAt": "2019-08-24T14:15:22Z"
}, - "esim": {
- "id": "string",
- "iccid": "string",
- "status": "string",
- "installTokenExpiresAt": "2019-08-24T14:15:22Z",
- "redeemedAt": "2019-08-24T14:15:22Z"
}, - "upstream": {
- "iccid": "string",
- "status": "string",
- "fetchedAt": "2019-08-24T14:15:22Z",
- "cached": true
}, - "upstreamOrder": {
- "orderId": "string",
- "orderStatus": "string",
- "subscription": {
- "status": "string",
- "activationTime": "string",
- "expiry": "string",
- "activateBy": "string",
- "upperLimitAmount": 0,
- "usedAmount": 0,
- "amountUnit": "string"
}, - "fetchedAt": "2019-08-24T14:15:22Z",
- "cached": true
}, - "esimHalf": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "topupHalf": {
- "codeId": "string",
- "code": "string",
- "status": "ISSUED",
- "redeemedAt": "2019-08-24T14:15:22Z",
- "redeemedEmail": "string",
- "esimId": "string",
- "iccid": "string",
- "subscriptionStatus": "PROVISIONED",
- "boundIccid": "string",
- "boundEmail": "string",
- "boundAt": "2019-08-24T14:15:22Z",
- "topupOrderId": "string",
- "chargebackedAt": "2019-08-24T14:15:22Z",
- "refundedAt": "2019-08-24T14:15:22Z",
- "revokedAt": "2019-08-24T14:15:22Z",
- "revokeReason": "string"
}, - "recentAuditEvents": [
- {
- "action": "string",
- "at": "2019-08-24T14:15:22Z",
- "customerId": "string",
- "after": { }
}
]
}| code required | string Human-displayable voucher code string. Must be the ESIM-half on bundle codes — TOPUP-halves have no install link. |
string Override destination email. Defaults to the on-disk | |
| locale | string Render locale for the install email template. Defaults to the eSIM row's stored locale, else |
| reason required | string Free-form operator-provided reason (recorded in the audit event). Surfaced in support tooling so the support agent can see why an admin resent. |
| resentBy required | string Operator handle (admin email / system actor). Recorded in the |
{- "code": "string",
- "email": "string",
- "locale": "string",
- "reason": "string",
- "resentBy": "string"
}{- "installUrl": "string",
- "sentTo": "string",
- "emailDelivered": true
}| code required | string Voucher code string (ESIM-half, TOPUP-half, or standalone). |
| disputeId required | string Stripe / payment-processor dispute identifier. Free-form text — recorded in audit so support can correlate the chargeback ticket. |
| reason required | string Free-form chargeback reason (fraud, friendly-fraud, etc). Recorded verbatim in the audit row. |
| actor required | string Operator handle (admin email / system actor) — recorded as |
{- "code": "string",
- "disputeId": "string",
- "reason": "string",
- "actor": "string"
}{- "code": "string",
- "esimId": "string",
- "terminatedAt": "2019-08-24T14:15:22Z",
- "cascaded": true
}| code required | string Voucher code string. ESIM-half refund cascades to the paired TOPUP-half. |
| reason required | string Free-form refund reason (defective device, customer dispute, etc.). Recorded verbatim on voucher_codes.refund_reason + audit. |
| refundedBy required | string Operator handle (admin email / system actor). Recorded as refunded_by on the voucher_codes row + audit. |
| notifyTraveller required | boolean Send a "your voucher has been refunded" email to the on-disk redeemed_email (bundle cascade sends one email per pair). Required — the operator must make an explicit choice per call. Set |
{- "code": "string",
- "reason": "string",
- "refundedBy": "string",
- "notifyTraveller": true
}{- "code": "string",
- "refundedAt": "2019-08-24T14:15:22Z",
- "cascaded": true,
- "cascadeKind": "refund"
}| code required | string Voucher code string (ESIM-half or standalone). TOPUP-half codes are rejected with 422. |
| reason required | string Free-form reissue reason (profile fault, scan failure, etc). Recorded in audit. |
| reissuedBy required | string Operator handle (admin email / system actor). Recorded in audit. |
| locale | string Render locale for the install email template. Defaults to the eSIM row's stored locale, else |
string Destination override for the new install email. Defaults to the on-disk redeemed_email. Use when support previously corrected the address via resend-install-email and the reissue should NOT fall back to the original (stale) one. |
{- "code": "string",
- "reason": "string",
- "reissuedBy": "string",
- "locale": "string",
- "email": "string"
}{- "code": "string",
- "action": "swapped",
- "esimId": "string",
- "iccid": "string",
- "installUrl": "string",
- "sentTo": "string",
- "emailDelivered": true
}{- "items": [
- {
- "reason": "hard_bounce",
- "suppressedAt": "2026-07-11T04:29:26.870Z",
- "firstNotificationId": "265d09b0-2099-5fc5-9cbb-6b7cb1b33d0b"
}
], - "total": 43,
- "limit": 100,
- "offset": 0
}{ }{- "deleted": true
}{ }{- "summary": {
- "removed": 42,
- "skipped": 1
}
}