Código
Ejemplos ejecutables para nuestras API públicas. Copia un archivo, añade tu propia clave y funciona contra los endpoints reales.
Affiliate API
Two read-only endpoints for affiliates. This is the affiliate programme, which pays commission on referred sales. It is not the Partner API, which resells at wholesale against a prepaid balance. The two systems are separate and a code from one does not work in the other.
Catalogue
GET https://buyukesim.com/api/affiliate/catalog
Authorization: Bearer YOUR_AFFILIATE_API_KEY
Every sellable product with the commission you earn on it. Optional ?location_code=GB narrows it to one destination. The key is shown in your affiliate panel.
Panel stats
GET https://buyukesim.com/api/affiliate_panel?code=YOURCODE&token=YOUR_PANEL_TOKEN
Clicks, orders, earnings and payout state. It takes your code plus the panel token rather than the API key, and it is rate limited, because a public stats endpoint is otherwise an invitation to guess other people's codes.
Referral links
Add ?ref=YOURCODE to any page. The landing is recorded, and a first-party cookie plus local storage carries the code through checkout, so a buyer who leaves and comes back later still counts as yours.
https://buyukesim.com/en/uk-esim/?ref=YOURCODE
Commission is calculated on the final price the customer paid, after any discount your link applies.
#!/usr/bin/env bash
# BuyUKeSIM Affiliate API. Both endpoints are read only.
set -euo pipefail
BASE="https://buyukesim.com/api"
# Catalogue with your commission per product. Needs the API key from your panel.
curl -s "$BASE/affiliate/catalog" \
-H "Authorization: Bearer ${BUYUKESIM_AFFILIATE_KEY:?export BUYUKESIM_AFFILIATE_KEY first}"
# One destination only.
curl -s "$BASE/affiliate/catalog?location_code=GB" \
-H "Authorization: Bearer ${BUYUKESIM_AFFILIATE_KEY}"
# Clicks, orders, earnings. Code plus panel token, not the API key.
curl -s "$BASE/affiliate_panel?code=${AFFILIATE_CODE:?export AFFILIATE_CODE}&token=${AFFILIATE_PANEL_TOKEN:?export AFFILIATE_PANEL_TOKEN}"
MCP Server
BuyUKeSIM exposes a Model Context Protocol server, so an AI assistant can read the catalogue, check coverage and place orders without a human driving a browser.
Endpoint
https://buyukesim.com/mcp
JSON-RPC 2.0 over POST. A GET returns 405 by design; that is the protocol, not an outage.
Protocol
Three methods matter: initialize, tools/list and tools/call. Protocol version is 2025-06-18.
Authentication
Read-only tools need no key. Anything that spends money, so the purchase tools and the wallet, needs an agent key beginning ak_live_, passed any of three ways:
Authorization: Bearer ak_live_...
X-API-Key: ak_live_...
or as an api_key argument inside the tool call. The key spends a prepaid balance, so treat it as a wallet: server side only, and never inside a prompt you would paste somewhere public.
Tools
| Tool | Needs a key | What it does |
|---|---|---|
get_uk_number_info | no | What the UK +44 line is, what it costs, what it does |
check_uk_coverage | no | Whether buyers reported the line working in a country |
list_covered_countries | no | Every country with buyer reports |
get_bundle_info | no | UK line plus travel data in one checkout |
list_vpn_plans | no | VPN durations and prices |
list_reviews | no | Published customer reviews |
check_order_status | no | Status of an order by id |
get_vpn_config | no | Config for a delivered VPN order |
purchase_uk_number | yes | Buy a UK line from the agent balance |
purchase_vpn | yes | Buy VPN access from the agent balance |
list_transactions | yes | Agent wallet history |
Why this exists
An assistant answering "get me a UK number that receives SMS" should be able to finish the job rather than describe it. The read-only half is open so an assistant can answer accurately about coverage and price without any relationship with us.
#!/usr/bin/env bash
# BuyUKeSIM MCP server over plain curl. JSON-RPC 2.0, POST only.
set -euo pipefail
MCP="https://buyukesim.com/mcp"
# Handshake. Announces the protocol version you speak.
curl -s -X POST "$MCP" -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"example-client","version":"1.0.0"}}
}'
# Every tool, with its input schema.
curl -s -X POST "$MCP" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# A read-only call. No key needed.
curl -s -X POST "$MCP" -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"check_uk_coverage","arguments":{"country":"DE"}}
}'
# Another one: what the product actually is, in the words we publish.
curl -s -X POST "$MCP" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_uk_number_info","arguments":{}}}'
# A spending call. Needs an agent key with a funded balance.
curl -s -X POST "$MCP" \
-H "Authorization: Bearer ${BUYUKESIM_AGENT_KEY:?export BUYUKESIM_AGENT_KEY first}" \
-H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":5,"method":"tools/call",
"params":{"name":"purchase_uk_number","arguments":{"quantity":1}}
}'
Partner API
Resell BuyUKeSIM products from your own storefront. One REST interface covers travel data eSIMs and VPN access. Orders are billed to your partner balance, so your customer never sees a crypto checkout.
The UK +44 number is not sold through this API. It is our own stock rather than a supplier product, and it is sold from buyukesim.com and through the affiliate programme - which pays commission on a referral instead of a wholesale price.
Base URL
https://buyukesim.com/api/v1
Authentication
Every request carries your partner key as a bearer token:
Authorization: Bearer YOUR_PARTNER_KEY
Keys are issued when your application is approved and are stored hashed on our side, so a key can be replaced but never read back. Treat it like a password: server side only, never in browser code, never in a public repository.
Do not have a key yet? POST /v1/signup starts an application.
Responses
Every success is wrapped, and the payload you want is under data:
{ "success": true, "data": { "order_id": 98421, "status": "paid" } }
Every failure carries a stable code, which is the part worth branching on:
{ "success": false, "error": { "code": "insufficient_balance", "message": "..." } }
Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /v1/signup | Apply for partner access |
| GET | /v1/balance | What is left on your prepaid balance |
| GET | /v1/reports | What you bought over a date range, and what it cost |
| GET | /v1/products | List everything you can sell, with your prices |
| GET | /v1/products?location_code=GB | Same list, filtered to one destination |
| POST | /v1/orders | Place an order |
| GET | /v1/orders/{id} | Order status and delivery payload |
| GET | /v1/orders/{id}/usage | Data used and remaining on a travel eSIM |
| GET | /v1/orders/{id}/vpn-config | WireGuard, OpenVPN or VLESS config for a VPN order |
Balance
Orders are billed to a prepaid balance. Every call that spends money already tells you where that leaves you:
{ "success": true, "data": { "order_id": 98421, "status": "paid",
"balance": { "charged_usd": 22.5, "remaining_usd": 477.5, "currency": "USD" } } }
Both numbers, not just the remainder: differencing two responses to work out what a call cost breaks the moment two of your workers order at the same time.
GET /v1/balance asks the same question away from a purchase - a top-up alarm, a dashboard tile, a batch job that refuses to start what it cannot pay for:
{ "success": true, "data": {
"balance_usd": 477.5, "currency": "USD", "discount_pct": 10,
"account": { "name": "Acme Telecom", "status": "active", "api_key_prefix": "buk_live_9f2a" } } }
An idempotent replay reports "charged_usd": 0 rather than omitting the block, so you never have to special-case one reply shape.
Reports
GET /v1/reports?from=2026-08-01&to=2026-08-31 is what you bought from us over a date range and what it cost, broken down by product:
{ "success": true, "data": {
"period": { "from": "2026-08-01", "to": "2026-08-31", "timezone": "UTC" },
"totals": { "orders": 4, "units": 4, "spend_usd": 25.16, "currency": "USD" },
"products": [
{ "type": "travel_data", "orders": 3, "units": 3, "spend_usd": 16.17 },
{ "type": "vpn", "orders": 1, "units": 1, "spend_usd": 8.99 }
],
"balance": { "remaining_usd": 477.5, "currency": "USD" },
"orders": [ { "order_id": 98423, "type": "vpn", "reference": "order-1236", "units": 1,
"spend_usd": 8.99, "created_at": "2026-08-29 20:00:00", "plan": "30d" } ],
"orders_truncated": false } }
Both dates are optional; without them you get the last 30 days. They are UTC calendar days on the order date and inclusive at both ends, so an order placed at 23:59 on to is in the report.
Paid orders only. An order whose fulfilment fails is cancelled and the balance is put straight back, so it never cost anything and counting it would make the report disagree with the account it is meant to explain.
units is what orders cannot say on its own: one UK order can carry several eSIMs, while travel and VPN are always one per order.
The itemised orders array is capped at 100 rows (limit, max 500, 0 for the summary alone). orders_truncated tells you the cap was hit; narrow the dates rather than raising the limit for a long history.
Prices are the ones frozen at order time. A later change to our retail price or to your discount never restates a past charge.
Ordering
POST /v1/orders takes a product_type and the fields that type needs:
{ "product_type": "travel_data", "package_code": "GB_1GB_7D", "reference": "order-1235" }
{ "product_type": "vpn", "vpn_plan": "30d", "reference": "order-1236" }
reference is your own order id. It comes back on every response for that order, which is what lets you reconcile without storing our ids.
Take package_code from /v1/products rather than composing it yourself. The catalogue moves: packages are retired and prices are recomputed, so a code that worked last month is not guaranteed to exist today.
Delivery
A travel eSIM is delivered as a QR code under data.esim:
{ "success": true, "data": { "order_id": 98422, "status": "paid",
"esim": { "ready": true, "items": [
{ "qr_code": "https://qr.example-provider.net/8e2bba37e8b94ca49bf23be334a0a57f.png",
"iccid": "8944...", "puk_code": null,
"manual_install": {
"activation_string": "LPA:1$rsp.example-provider.com$1173094A3D3A473D84695E0D6C404B34",
"smdp_address": "rsp.example-provider.com",
"activation_code": "1173094A3D3A473D84695E0D6C404B34",
"confirmation_code_required": false
} }
] } } }
puk_code is null on a travel eSIM. The hosts above are placeholders: the real QR host and SM-DP+ address depend on which carrier serves that destination, so read them from the response and never hard-code or pattern-match one.
Show both ways in, not just the QR
manual_install is the same profile as the QR, in the form a phone asks for under "Enter details manually". Put it in your UI beside the QR rather than behind a "having trouble?" link: a customer reading your app on the phone the profile has to install to cannot scan a code off their own screen, and if the QR is all you show them, that is a dead end with a support ticket at the end of it.
activation_string is the whole LPA: payload, for a one-tap handoff or for re-encoding the QR yourself. smdp_address and activation_code are the two boxes a customer types into. Leave the phone's *confirmation code* box empty - our profiles do not use one, and that blank box is where manual installs stall.
It is null when we do not hold a decoded activation string for that profile, which is rare but real. Treat null as "QR only" rather than assuming the fields exist.
Poll GET /v1/orders/{id} until data.esim.ready is true. Do not poll for status: it reads paid the moment your balance is taken, which is a payment state, not a delivery one. Fulfilment is usually seconds, but a supplier can be slow, so treat anything other than cancelled or expired as still in progress rather than as an error.
VPN orders carry no eSIM. Their credentials come back on the create response under data.vpn, and the config file itself from GET /v1/orders/{id}/vpn-config.
Errors
Standard HTTP status codes, with a stable error.code in the body. A 402 (insufficient_balance) means your partner balance is too low to cover the order, not that anything is wrong with the request. A 502 means we took nothing: the balance is put back and the order is cancelled.
Send a reference on anything that spends money. It is your own id, and sending the same one twice returns the FIRST order with "idempotent_replay": true instead of buying a second one - which is what makes a timed-out request safe to retry.
#!/usr/bin/env bash
# BuyUKeSIM Partner API - every endpoint, with curl.
# Replace the key with your own. Keep it server side.
set -euo pipefail
BASE="https://buyukesim.com/api/v1"
KEY="${BUYUKESIM_PARTNER_KEY:?export BUYUKESIM_PARTNER_KEY first}"
AUTH="Authorization: Bearer ${KEY}"
# --- Account ---------------------------------------------------------------
# What is left on your prepaid balance, plus your discount rate. Every order returns
# the same figure in its own `balance` block, so this call is for the checks that
# happen away from a purchase - a top-up alarm, a nightly job.
curl -s -H "$AUTH" "$BASE/balance"
# What you bought and what it cost, by product. Dates are UTC calendar days on the
# order date, inclusive at both ends; leave them off for the last 30 days.
curl -s -H "$AUTH" "$BASE/reports?from=2026-08-01&to=2026-08-31"
# Summary only, no itemised rows.
curl -s -H "$AUTH" "$BASE/reports?from=2026-08-01&to=2026-08-31&limit=0"
# --- Catalogue -------------------------------------------------------------
# Everything you can sell, at your partner prices.
curl -s -H "$AUTH" "$BASE/products"
# Narrow it to one destination. Use the ISO code, GB for the United Kingdom.
curl -s -H "$AUTH" "$BASE/products?location_code=GB"
# --- Ordering --------------------------------------------------------------
# Travel data. Take package_code from /products, never hand-compose it.
curl -s -X POST "$BASE/orders" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"product_type":"travel_data","package_code":"GB_1GB_7D","reference":"order-1235"}'
# VPN access for a fixed number of days.
curl -s -X POST "$BASE/orders" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"product_type":"vpn","vpn_plan":"30d","reference":"order-1236"}'
# --- After the order -------------------------------------------------------
ORDER_ID=98421
# Status plus the delivery payload once it is ready. Each eSIM item carries both
# ways in: `qr_code` for scanning, and `manual_install` with the SM-DP+ address and
# activation code for a customer who is reading your app on the phone the profile
# has to install to and has no second screen to point a camera at.
curl -s -H "$AUTH" "$BASE/orders/$ORDER_ID"
# Data used and remaining. Travel eSIM orders only.
curl -s -H "$AUTH" "$BASE/orders/$ORDER_ID/usage"
# VPN config. protocol is wireguard, openvpn or vless.
curl -s -H "$AUTH" "$BASE/orders/$ORDER_ID/vpn-config?protocol=wireguard&server_id=42"
# --- Applying for access ---------------------------------------------------
# No key yet? This starts an application. No auth header on this one.
curl -s -X POST "$BASE/signup" \
-H 'Content-Type: application/json' \
-d '{"company":"Example Ltd","email":"you@example.com","website":"https://example.com"}'
/**
* BuyUKeSIM Partner API - Node 18+, no dependencies.
*
* Run: BUYUKESIM_PARTNER_KEY=... node node.mjs
*/
const BASE = 'https://buyukesim.com/api/v1';
const KEY = process.env.BUYUKESIM_PARTNER_KEY;
if (!KEY) throw new Error('set BUYUKESIM_PARTNER_KEY');
async function api(path, { method = 'GET', body } = {}) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json().catch(() => null);
if (!res.ok) {
// Errors are { success: false, error: { code, message } } - the code is the part worth
// branching on. 402 is a low partner balance, not a malformed request.
throw new Error(`${res.status} ${payload?.error?.code ?? ''} ${payload?.error?.message ?? res.statusText}`.trim());
}
// Every success is wrapped: { success: true, data: { ... } }. Unwrap once, here, so no
// caller has to remember the envelope.
return payload?.data ?? payload;
}
/**
* Everything sellable, at your prices: { discount_pct, travel_data[], vpn }.
* `travel_data` is the array of destination packages, about 198 of them.
*/
const listProducts = (locationCode) =>
api(`/products${locationCode ? `?location_code=${encodeURIComponent(locationCode)}` : ''}`);
/** What is left on the prepaid account: { balance_usd, currency, discount_pct, account }. */
const balance = () => api('/balance');
/**
* What you bought between two dates and what it cost, broken down by product.
*
* Dates are UTC calendar days on the order date and inclusive at both ends; omit them
* for the last 30 days. Paid orders only: a failed order is refunded, so it never cost
* anything.
*
* `limit` caps the itemised `orders` array (default 100, max 500, 0 for summary only).
*/
const report = ({ from, to, limit } = {}) => {
const q = new URLSearchParams();
if (from) q.set('from', from);
if (to) q.set('to', to);
if (limit !== undefined) q.set('limit', String(limit));
const qs = q.toString();
return api(`/reports${qs ? `?${qs}` : ''}`);
};
const createOrder = (payload) => api('/orders', { method: 'POST', body: payload });
const orderStatus = (id) => api(`/orders/${id}`);
const dataUsage = (id) => api(`/orders/${id}/usage`);
const vpnConfig = (id, protocol = 'wireguard', serverId) =>
api(`/orders/${id}/vpn-config?protocol=${protocol}${serverId ? `&server_id=${serverId}` : ''}`);
/**
* Wait for the eSIM to exist.
*
* `status` is "paid" the moment the balance is taken, which is not the same as delivered -
* `esim.ready` is what says the QR is there. Anything that is not an explicit failure counts
* as still in progress: a slow supplier is normal, and treating "not ready yet" as an error
* is the usual way integrations end up ordering twice.
*/
async function waitForDelivery(id, { timeoutMs = 120000, everyMs = 3000 } = {}) {
const deadline = Date.now() + timeoutMs;
for (;;) {
const order = await orderStatus(id);
if (order.esim?.ready) return order;
if (order.status === 'cancelled' || order.status === 'expired') {
throw new Error(`order ${id} ${order.status}`);
}
if (Date.now() > deadline) throw new Error(`order ${id} still ${order.status} after timeout`);
await new Promise((r) => setTimeout(r, everyMs));
}
}
// --- Example run -----------------------------------------------------------
const products = await listProducts('GB');
console.log(`${products.travel_data?.length ?? 0} travel packages for GB, your discount ${products.discount_pct}%`);
// Take the code from the catalogue. Hand-composed codes go stale.
const pkg = products.travel_data?.[0];
// The API sells travel data and VPN. Without a package there is nothing to buy, so bail
// rather than falling back to a product this account may not have.
if (!pkg) throw new Error('no travel package available for GB');
const order = await createOrder({
product_type: 'travel_data',
package_code: pkg.package_code,
reference: `order-${Date.now()}`,
});
// Every call that spends money says what it cost and what survived it, so a low balance
// surfaces on the order that caused it rather than on the next one that fails with a 402.
console.log('placed', order.order_id, 'ref', order.reference);
console.log(`charged $${order.balance.charged_usd}, $${order.balance.remaining_usd} left`);
const delivered = await waitForDelivery(order.order_id);
const [first] = delivered.esim?.items ?? [];
console.log('QR:', first?.qr_code, 'ICCID:', first?.iccid);
// Show BOTH routes in your own UI. A customer reading this on the phone the profile has to
// install to cannot scan the QR off their own screen, and the manual fields are the way out
// of that. Leave the phone's "confirmation code" box empty - our profiles do not use one,
// and that blank box is where manual installs stall.
if (first?.manual_install) {
const m = first.manual_install;
console.log('SM-DP+:', m.smdp_address);
console.log('Activation code:', m.activation_code);
console.log('One-tap:', m.activation_string);
}
// Travel orders only.
if (delivered.product.type === 'travel_data') {
console.log('usage:', await dataUsage(order.order_id));
}
// VPN orders only. A VPN order carries no eSIM, so it never goes through waitForDelivery:
// the credentials are already on the create response, under data.vpn.
if (order.product.type === 'vpn') {
console.log('config:', await vpnConfig(order.order_id, 'wireguard'));
}
// Month end: what you bought and what it cost, straight from our ledger rather than yours.
const august = await report({ from: '2026-08-01', to: '2026-08-31' });
console.log(`spent $${august.totals.spend_usd} on ${august.totals.orders} order(s)`);
for (const line of august.products) {
console.log(` ${line.units} x ${line.type} = $${line.spend_usd}`);
}
console.log('balance now', (await balance()).balance_usd);
"""
BuyUKeSIM Partner API - Python 3.9+, requests only.
Run: BUYUKESIM_PARTNER_KEY=... python python.py
"""
import os
import time
from typing import Any, Optional
import requests
BASE = "https://buyukesim.com/api/v1"
KEY = os.environ.get("BUYUKESIM_PARTNER_KEY")
if not KEY:
raise SystemExit("set BUYUKESIM_PARTNER_KEY")
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {KEY}"})
class PartnerApiError(RuntimeError):
"""Carries the HTTP status so 402 (low balance) can be handled on its own."""
def __init__(self, status: int, message: str, code: str = "") -> None:
super().__init__(f"{status} {code} {message}".replace(" ", " ").strip())
self.status = status
self.code = code
def api(path: str, method: str = "GET", body: Optional[dict] = None) -> Any:
res = session.request(method, f"{BASE}{path}", json=body, timeout=30)
try:
payload = res.json()
except ValueError:
payload = None
if not res.ok:
# Errors are {"success": false, "error": {"code", "message"}} - the code is the part
# worth branching on.
err = (payload or {}).get("error") or {}
raise PartnerApiError(res.status_code, err.get("message", res.reason), err.get("code", ""))
# Every success is wrapped: {"success": true, "data": {...}}. Unwrap once, here.
return (payload or {}).get("data", payload)
def list_products(location_code: Optional[str] = None) -> Any:
"""
Everything sellable, at your prices: {discount_pct, travel_data[], vpn}.
`travel_data` is the array of destination packages, about 198 of them.
"""
query = f"?location_code={location_code}" if location_code else ""
return api(f"/products{query}")
def balance() -> Any:
"""What is left on the prepaid account: balance_usd, currency, discount_pct, account."""
return api("/balance")
def report(
date_from: Optional[str] = None,
date_to: Optional[str] = None,
limit: Optional[int] = None,
) -> Any:
"""
What you bought between two dates and what it cost, broken down by product.
Dates are UTC calendar days on the order date and inclusive at both ends; omit them
for the last 30 days. Paid orders only: a failed order is refunded, so it never cost
anything.
`limit` caps the itemised `orders` array (default 100, max 500, 0 for summary only).
"""
parts = []
if date_from:
parts.append(f"from={date_from}")
if date_to:
parts.append(f"to={date_to}")
if limit is not None:
parts.append(f"limit={limit}")
query = ("?" + "&".join(parts)) if parts else ""
return api(f"/reports{query}")
def create_order(payload: dict) -> Any:
return api("/orders", "POST", payload)
def order_status(order_id: int) -> Any:
return api(f"/orders/{order_id}")
def data_usage(order_id: int) -> Any:
return api(f"/orders/{order_id}/usage")
def vpn_config(order_id: int, protocol: str = "wireguard", server_id: Optional[int] = None) -> Any:
query = f"?protocol={protocol}" + (f"&server_id={server_id}" if server_id else "")
return api(f"/orders/{order_id}/vpn-config{query}")
def wait_for_delivery(order_id: int, timeout: int = 120, every: int = 3) -> Any:
"""
Poll until the eSIM exists.
`status` is "paid" the moment the balance is taken, which is not the same as delivered:
`esim.ready` is what says the QR is there. Only an explicit failure is an error - a slow
supplier is normal, and treating "not ready yet" as a failure is how integrations end up
ordering twice.
"""
deadline = time.time() + timeout
while True:
order = order_status(order_id)
if (order.get("esim") or {}).get("ready"):
return order
if order.get("status") in ("cancelled", "expired"):
raise RuntimeError(f"order {order_id} {order['status']}")
if time.time() > deadline:
raise TimeoutError(f"order {order_id} still {order.get('status')}")
time.sleep(every)
if __name__ == "__main__":
catalogue = list_products("GB")
packages = catalogue.get("travel_data", [])
print(f"{len(packages)} travel packages for GB, your discount {catalogue.get('discount_pct')}%")
# Always take package_code from the catalogue; codes are retired over time.
pkg = packages[0] if packages else None
ref = f"order-{int(time.time())}"
# The API sells travel data and VPN. Without a package there is nothing to buy, so stop
# rather than falling back to a product this account may not have.
if not pkg:
raise SystemExit("no travel package available for GB")
payload = {"product_type": "travel_data", "package_code": pkg["package_code"], "reference": ref}
order = create_order(payload)
print("placed", order["order_id"], "ref", order["reference"])
# Every call that spends money reports what it cost and what survived it, so a low
# balance shows up on the order that caused it, not on the next one that 402s.
print(f"charged ${order['balance']['charged_usd']}, ${order['balance']['remaining_usd']} left")
delivered = wait_for_delivery(order["order_id"])
items = (delivered.get("esim") or {}).get("items") or []
if items:
print("QR:", items[0]["qr_code"], "ICCID:", items[0]["iccid"])
# Show BOTH routes in your own UI. A customer reading this on the phone the profile
# has to install to cannot scan the QR off their own screen, and these fields are the
# way out of that. Leave the phone's "confirmation code" box empty - our profiles do
# not use one, and that blank box is where manual installs stall.
manual = items[0].get("manual_install")
if manual:
print("SM-DP+:", manual["smdp_address"])
print("Activation code:", manual["activation_code"])
print("One-tap:", manual["activation_string"])
if delivered["product"]["type"] == "travel_data":
print("usage:", data_usage(order["order_id"]))
# A VPN order carries no eSIM, so it never goes through wait_for_delivery: the
# credentials are already on the create response, under data.vpn.
if order["product"]["type"] == "vpn":
print("config:", vpn_config(order["order_id"]))
# Month end: what you bought and what it cost, from our ledger rather than yours.
august = report("2026-08-01", "2026-08-31")
print(f"spent ${august['totals']['spend_usd']} on {august['totals']['orders']} order(s)")
for line in august["products"]:
print(f" {line['units']} x {line['type']} = ${line['spend_usd']}")
print("balance now", balance()["balance_usd"])
Generado 2026-09-01
