Vio eSIM
REST API · v1

Vio eSIM API Reference

Two REST surfaces off the same backend: the customer-facing API that powers the web app and our iOS/Android apps, and a separate API-key-authenticated Partner API for reselling Vio eSIM programmatically.

Mobile / Web API
https://vioesim.com/api/v1
Partner API
https://vioesim.com/api/public/v1

Authenticating the mobile app

Both the web app and the native apps share one session system — a native app just carries the token itself instead of relying on a cookie jar.

Register or log in. Both endpoints return a token field alongside the user object (and a Set-Cookie header the web app uses instead). Store token in Keychain (iOS) or the Keystore-backed store (Android).

Send it back on every request:

Authorization: Bearer <token>

Token lifetime: 30 days from issue, or until /api/v1/auth/logout is called. There's no refresh-token step — a near-expiry token is simply replaced by asking the user to log in again.

A session on a suspended, banned, or deleted account stops working immediately — checked server-side on every request, even if the token itself hasn't expired.

Authenticating the Partner API

For external businesses reselling Vio eSIM through their own site or app. A partner is a normal Vio eSIM account: generate a key from Dashboard → API Keys (one-time reveal — store it, it can't be shown again), then send it on every request:

Authorization: Bearer vio_live_sk_...
# or
X-API-Key: vio_live_sk_...

Generating a new key immediately invalidates the previous one.

Billing model: Partner API orders are charged synchronously against the account's prepaid wallet balance — no checkout redirect, since this is a server-to-server call. Top up from the dashboard (card/crypto) before placing orders; POST /orders returns 402 if the balance is insufficient.

Quick start — Partner API

Browse the catalog and buy an eSIM in two calls:

curl https://vioesim.com/api/public/v1/destinations \
  -H "Authorization: Bearer vio_live_sk_..."

curl -X POST https://vioesim.com/api/public/v1/orders \
  -H "Authorization: Bearer vio_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "planId": "cus...", "quantity": 1 }'

Errors & rate limits

The Mobile/Web API returns { "error": "message" } (currently Turkish-language messages). The Partner API returns a structured shape so you can branch on code:

{ "error": { "code": "INSUFFICIENT_BALANCE", "message": "Insufficient wallet balance." } }
StatusMeaning
401Missing/invalid/expired token or API key
402Partner API only — wallet balance too low
404Resource not found, or not owned by the caller
409Conflicting state (e.g. deleting an account with a non-zero balance)
429Rate limited — see Retry-After header (seconds)
502Payment captured but eSIM provisioning failed — don't retry the charge

Rate limits (Partner API): 120 req/min per key on reads, 30 req/min on POST /orders.

Mobile / Web API

Auth

POST/api/v1/auth/registerNo auth

Register

Creates an account and an active session.

Request body
{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "[email protected]",
  "password": "min 8 chars"
}
200 OK
{
  "success": true,
  "user": {
    "id", "firstName", "lastName", "fullName", "email",
    "emailVerified", "walletBalance", "currency", "locale", "createdAt"
  },
  "token": "..."
}
POST/api/v1/auth/loginNo auth

Login

Body: { email, password }. Same response shape as Register. 401 on bad credentials, 403 if the account is suspended.

POST/api/v1/auth/logoutSession token

Logout

No body. Deletes the session server-side — call this on real logout, not just "forget the token locally", so a stolen token can't keep working.

GET/api/v1/auth/meSession token · optional

Current user

Returns { "user": null } (never an error) when logged out — use this on app launch to decide whether to show the login screen.

POST/api/v1/auth/forgot-passwordNo auth

Forgot password

Body: { email, locale }. Always returns { success: true } regardless of whether the address exists, so it can't be used to enumerate accounts. Sends an email with a reset link/token (1-hour expiry).

POST/api/v1/auth/reset-passwordNo auth

Reset password

Body: { token, newPassword }. GET with ?token= checks validity before showing the form ({ "valid": true|false }).

POST/api/v1/auth/change-passwordSession token

Change password

Body: { currentPassword, newPassword }. currentPassword is required unless the account has no password set yet (e.g. social sign-in).

Account

PATCH/api/v1/user/profileSession token

Update profile

Send only the fields you want to change.

FieldNotes
firstName, lastNameoptional · string
preferredCurrencyoptional · one of USD EUR GBP TRY
localeoptional · 2-letter code, e.g. en
200 OK
{ "success": true, "user": { ...same shape as /auth/me } }
POST/api/v1/user/avatarSession token

Update avatar

Body: { "avatarUrl": "data:image/..." } — a base64 data URI, max ~1.5MB.

POST/api/v1/user/api-key/generateSession token

Generate API key

Issues a new Partner API key for this account, replacing any previous one. The raw key is shown once, in this response — only its prefix is ever retrievable again.

200 OK
{
  "success": true,
  "key": "vio_live_sk_...",
  "apiKeyPrefix": "vio_live_sk_ab12…9f8e",
  "apiKeyGeneratedAt": "2026-08-22T17:00:00.781Z"
}
DELETE/api/v1/user/accountSession token

Delete account

Required for App Store review (account-creation apps must offer in-app deletion). Anonymizes the account (email scrambled and freed for re-registration, name/avatar/password cleared) rather than hard-deleting rows, so past orders stay on the books for tax/accounting records. Destroys every session and registered push device for the user.

Returns 409 if walletBalance > 0 — the app should tell the user to withdraw or contact support first rather than silently forfeiting funds.

Catalog

GET/api/v1/destinationsNo auth

List destinations

Countries and regions with active plans, priced and UI-formatted (this is what the storefront itself calls). Optional ?filter=popular|regional|global|<search text>.

GET/api/v1/destinations/:codeNo auth

Destination detail

Full detail (description, plans) for one country by ISO code, e.g. /api/v1/destinations/jp.

Purchases

POST/api/v1/checkoutSession token

Checkout

Buys a plan directly (as opposed to depositing into the wallet first).

FieldNotes
planId*
quantityoptional · 1–10, default 1
paymentMethod*wallet | stripe | crypto
successUrl, cancelUrloptional · for stripe/crypto redirects. Use a universal/app link, not a bare web URL, so the redirect reopens the app.
200 OK — wallet (synchronous)
{ "success": true, "orderId": "..." }
200 OK — stripe / crypto (redirect required)
{ "success": true, "orderId": "...", "url": "https://checkout.stripe.com/..." }
Open url in an in-app browser (SFSafariViewController / Chrome Custom Tabs) rather than a bare WebView — both Stripe and Cryptomus expect a real browser context.
POST/api/v1/payments/stripe/create-sessionSession token

Stripe wallet top-up

Tops up the wallet by card — a separate flow from Checkout. Body: { amount (cents, min 200), currency, successUrl, cancelUrl }. Returns { sessionId, url }.

POST/api/v1/payments/cryptomus/create-invoiceSession token

Crypto wallet top-up

Body: { amount (decimal string, min $2), currency, url_return }. Returns the Cryptomus invoice object including url.

GET/api/v1/user/ordersSession token

List orders

All of the caller's orders, newest first.

GET/api/v1/user/orders/:idSession token

Order detail

One order, including esimIds — fetch full eSIM detail (QR, activation code) via GET /api/v1/user/esims and match by id.

GET/api/v1/user/esimsSession token

My eSIMs

Every eSIM the caller owns: iccid, activationCode, qrCodeUrl, smdpAddress, status, dataUsage/totalVolume (MB), activatedAt, expiresAt. This is what drives the "My eSIMs" screen and QR display.

GET/api/v1/user/wallet/transactionsSession token

Wallet history

Deposit/withdrawal/purchase/refund history for the wallet balance shown in /auth/me.

Support

GET/api/v1/support/ticketsSession token

List tickets

The caller's tickets with their full message threads, newest first.

POST/api/v1/support/ticketsSession token

New ticket

Body: { subject, category, message } (category optional).

POST/api/v1/support/tickets/:id/messagesSession token

Reply to ticket

Body: { message }. Replying to a RESOLVED/CLOSED ticket automatically reopens it.

Push notification devices

Registers device tokens only — sending pushes needs APNs/FCM credentials, see the callout below.

POST/api/v1/user/push-devicesSession token

Register device

Call after obtaining an APNs or FCM token. Body: { platform: "IOS"|"ANDROID", pushToken, appVersion }. Upserts by token, so calling it again on every app launch is fine and recommended (tokens can rotate).

DELETE/api/v1/user/push-devicesSession token

Unregister device

Body: { pushToken }. Call on logout so a shared/reset device stops receiving another user's notifications.

Partner API

Everything below requires the API-key header described above — no session cookie applies here.

Catalog

GET/api/public/v1/destinationsAPI key

List destinations

Full catalog, in a partner-oriented shape (not the storefront's UI shape).

200 OK
{
  "countries": [
    {
      "code": "JP",
      "name": { "en": "Japan", "tr": "Japonya", "...": "..." },
      "flagEmoji": "🇯🇵",
      "continent": "Asia",
      "plans": [
        { "id": "cus...", "dataAmountMb": 1024, "durationDays": 7, "priceUsd": 4.25, "isTopUp": false }
      ]
    }
  ],
  "regions": [ { "code": "...", "name": {...}, "countryCount": 15, "plans": [...] } ]
}
GET/api/public/v1/destinations/:codeAPI key

Destination detail

One country by ISO code (e.g. /api/public/v1/destinations/JP), same plan shape as above.

Orders

POST/api/public/v1/ordersAPI key

Create order

Buys a plan against the caller's wallet balance and provisions the eSIM synchronously — the response already contains the QR/activation data, ready to hand to your end customer. No polling needed for the happy path.

Request body
{ "planId": "cus...", "quantity": 1 }
201 Created
{
  "id": "...", "orderNumber": "VIO-API-...", "status": "COMPLETED", "amountUsd": 4.25,
  "esims": [
    { "id": "...", "iccid": "...", "activationCode": "...",
      "qrCodeUrl": "https://...", "smdpAddress": "...", "status": "PENDING" }
  ]
}
Failure modes: 402 INSUFFICIENT_BALANCE (top up first), 404 PLAN_NOT_FOUND, or 502 FULFILLMENT_FAILED (charged, but the provider failed to issue the eSIM — the order id is included, support is auto-notified; poll GET /orders/:id rather than re-charging).
GET/api/public/v1/ordersAPI key

List orders

Optional ?limit= (default 25, max 100). Summary shape — use the detail endpoint for eSIM data.

GET/api/public/v1/orders/:idAPI key

Order detail

Full order detail including every eSIM's QR/activation data and live usage (dataUsageMb / totalVolumeMb) — poll this to show your customer their remaining data.

Wallet

GET/api/public/v1/walletAPI key

Wallet balance

Poll before placing a large batch of orders, or alert yourself when it's running low.

200 OK
{ "balanceUsd": 128.40, "currency": "USD" }

What isn't built yet

So nothing here is assumed working that isn't.

Push notification delivery

Token registration works; actually sending needs APNs/FCM credentials.

Partner-specific pricing

The Partner API currently charges the same retail price as the storefront. Wholesale/negotiated pricing would need a schema change plus a business decision.

Refresh tokens

Sessions are long-lived (30-day) flat tokens, not short-lived-access + refresh-token pairs. Fine for now; revisit for a stricter security posture later.

Native Stripe payment sheet

Stripe integration is hosted Checkout (redirect/webview). A native card-entry UI would use PaymentIntents + Stripe's SDK instead.

Webhooks for partners

Partners must poll GET /orders/:id for status; there's no outbound webhook (e.g. "eSIM activated") yet.

Vio eSIM API Reference & Partner API Docs