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.
https://vioesim.com/api/v1https://vioesim.com/api/public/v1Authenticating 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.
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.
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." } }| Status | Meaning |
|---|---|
| 401 | Missing/invalid/expired token or API key |
| 402 | Partner API only — wallet balance too low |
| 404 | Resource not found, or not owned by the caller |
| 409 | Conflicting state (e.g. deleting an account with a non-zero balance) |
| 429 | Rate limited — see Retry-After header (seconds) |
| 502 | Payment 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
Register
Creates an account and an active session.
{
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"password": "min 8 chars"
}{
"success": true,
"user": {
"id", "firstName", "lastName", "fullName", "email",
"emailVerified", "walletBalance", "currency", "locale", "createdAt"
},
"token": "..."
}Login
Body: { email, password }. Same response shape as Register. 401 on bad credentials, 403 if the account is suspended.
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.
Current user
Returns { "user": null } (never an error) when logged out — use this on app launch to decide whether to show the login screen.
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).
Reset password
Body: { token, newPassword }. GET with ?token= checks validity before showing the form ({ "valid": true|false }).
Change password
Body: { currentPassword, newPassword }. currentPassword is required unless the account has no password set yet (e.g. social sign-in).
Account
Update profile
Send only the fields you want to change.
| Field | Notes |
|---|---|
| firstName, lastName | optional · string |
| preferredCurrency | optional · one of USD EUR GBP TRY |
| locale | optional · 2-letter code, e.g. en |
{ "success": true, "user": { ...same shape as /auth/me } }Update avatar
Body: { "avatarUrl": "data:image/..." } — a base64 data URI, max ~1.5MB.
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.
{
"success": true,
"key": "vio_live_sk_...",
"apiKeyPrefix": "vio_live_sk_ab12…9f8e",
"apiKeyGeneratedAt": "2026-08-22T17:00:00.781Z"
}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.
Catalog
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>.
Destination detail
Full detail (description, plans) for one country by ISO code, e.g. /api/v1/destinations/jp.
Purchases
Checkout
Buys a plan directly (as opposed to depositing into the wallet first).
| Field | Notes |
|---|---|
| planId* | |
| quantity | optional · 1–10, default 1 |
| paymentMethod* | wallet | stripe | crypto |
| successUrl, cancelUrl | optional · for stripe/crypto redirects. Use a universal/app link, not a bare web URL, so the redirect reopens the app. |
{ "success": true, "orderId": "..." }{ "success": true, "orderId": "...", "url": "https://checkout.stripe.com/..." }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 }.
Crypto wallet top-up
Body: { amount (decimal string, min $2), currency, url_return }. Returns the Cryptomus invoice object including url.
List orders
All of the caller's orders, newest first.
Order detail
One order, including esimIds — fetch full eSIM detail (QR, activation code) via GET /api/v1/user/esims and match by id.
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.
Wallet history
Deposit/withdrawal/purchase/refund history for the wallet balance shown in /auth/me.
Support
List tickets
The caller's tickets with their full message threads, newest first.
New ticket
Body: { subject, category, message } (category optional).
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.
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).
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
List destinations
Full catalog, in a partner-oriented shape (not the storefront's UI shape).
{
"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": [...] } ]
}Destination detail
One country by ISO code (e.g. /api/public/v1/destinations/JP), same plan shape as above.
Orders
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.
{ "planId": "cus...", "quantity": 1 }{
"id": "...", "orderNumber": "VIO-API-...", "status": "COMPLETED", "amountUsd": 4.25,
"esims": [
{ "id": "...", "iccid": "...", "activationCode": "...",
"qrCodeUrl": "https://...", "smdpAddress": "...", "status": "PENDING" }
]
}List orders
Optional ?limit= (default 25, max 100). Summary shape — use the detail endpoint for eSIM data.
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
Wallet balance
Poll before placing a large batch of orders, or alert yourself when it's running low.
{ "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.
