Pay-As-You-Go (PAYG) eSIM
Your customer pays you upfront; you fund their SIM with that amount.
Customer pays you $40 for a travel eSIM. You keep $10, fund the SIM with $30, and the SIM drains as they use data at the rates in /rates. When the wallet hits $0, the SIM cuts off on its own — you're never charged again for that SIM after the fund call.
// After collecting payment on your side (e.g., customer paid $40, you fund $30):
const esim = await fetch('https://citrusmobile.com/api/v2/reseller/esim/provision', {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
end_user_reference: 'customer_456',
label: 'Maria G. - PAYG Plan',
}),
}).then(r => r.json());
// Fund the SIM's wallet. Your reseller account is debited once, right here.
// Data usage drains the SIM wallet — your account is NOT charged again.
await fetch(`https://citrusmobile.com/api/v2/reseller/esim/${esim.iccid}/fund`, {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 30 }),
});
// esim.qr_code → base64 PNG to display/email
// esim.direct_install_url → iOS 17.4+ one-tap install link// Email the QR code and install link
await sendEmail({
to: customer.email,
subject: 'Your eSIM is ready',
html: `
<p>Scan this QR code in Settings > Cellular > Add eSIM:</p>
<img src="${esim.qr_code}" />
<p>Or tap to install: <a href="${esim.direct_install_url}">Install eSIM</a></p>
`,
});// esim.balance_low fires once when the SIM wallet crosses below $5.
// esim.balance_depleted fires when it hits $0 and the network cuts the SIM off.
// balance.low / balance.depleted are for YOUR reseller top-up account.
await fetch('https://citrusmobile.com/api/v2/reseller/webhooks', {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://your-server.com/webhooks/citrus',
events: [
'esim.balance_low', // "Your SIM is running low — top up?"
'esim.balance_depleted', // "Your SIM ran out — top up to restore"
'balance.low', // Your reseller account is running low on top-up funds
'balance.depleted',
],
}),
});
// Then in your webhook handler:
app.post('/webhooks/citrus', (req, res) => {
const e = req.body;
if (e.event === 'esim.balance_low') {
const order = await db.orders.findOne({ iccid: e.data.iccid });
await sendEmail({
to: order.customer_email,
subject: 'Your eSIM is running low',
html: `Only $${e.data.wallet_balance_usd} left. Top up to stay connected.`,
});
}
res.sendStatus(200);
});Your margin is customer_paid − fund_amount. Data usage drains the SIM's wallet at the rates in /rates; your reseller account is only debited once, at fund time. To top a customer up mid-trip, just call /fund again with the additional amount.
Unlimited Data with Fair-Use Throttling
Sell a flat-rate "unlimited" plan: full speed for the first 2 GB/day, then throttled to 512 Kbps.
Customer pays you a flat monthly fee (say $25). You provision an eSIM and fund it with a generous ceiling (say $50) that's high enough to cover a month of reasonable usage but bounded so a runaway customer can't bleed you out. A throttle cron caps their daily burn at 2 GB of full-speed data; after that they stay connected at 512 Kbps. When the SIM wallet hits $0 or the plan period ends, the SIM cuts off.
// Provision
const esim = await citrus.post('/esim/provision', {
end_user_reference: 'customer_789',
label: 'Unlimited EU Roaming - 30 days',
});
// Fund with a generous ceiling — high enough to feel unlimited,
// bounded enough to cap your worst-case exposure on this one SIM.
await citrus.post(`/esim/${esim.iccid}/fund`, { amount: 50 });
// Save the "starting wallet" so your throttle cron knows today's baseline.
// wallet_balance_usd rounds DOWN, so the fund_baseline may be ~1-5¢ below
// the amount you sent — that's expected.
const fresh = await citrus.get(`/esim/${esim.iccid}`);
await db.unlimitedPlans.insert({
iccid: esim.iccid,
started_at: new Date(),
fund_baseline_usd: fresh.wallet_balance_usd,
day_start_baseline_usd: fresh.wallet_balance_usd,
});
// Deliver QR code to customer// Pull the SIM's current wallet_balance_usd and compare it against the
// baseline we stored at the start of today. Difference = usage this day.
const DAILY_CAP_USD = 5.00; // ~2 GB in most EU countries at the rates in /rates
const THROTTLE_SPEED = 'SPEED_500_KBPS'; // 512 Kbps after cap
async function checkAndThrottle(iccid) {
const res = await fetch(
`https://citrusmobile.com/api/v2/reseller/esim/${iccid}`,
{ headers: { 'Authorization': 'Bearer rsk_your_api_key' } }
);
const esim = await res.json();
// How much the SIM wallet has drained since today's baseline.
const plan = await db.unlimitedPlans.findOne({ iccid });
const usedToday = plan.day_start_baseline_usd - esim.wallet_balance_usd;
if (usedToday >= DAILY_CAP_USD) {
await fetch(`https://citrusmobile.com/api/v2/reseller/esim/${iccid}/throttle`, {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ speed: THROTTLE_SPEED }),
});
console.log(`Throttled ${iccid} after $${usedToday.toFixed(2)} burn today`);
}
}// Run at 00:00 in the customer's timezone
async function resetDailyBaselines() {
const res = await fetch(
'https://citrusmobile.com/api/v2/reseller/esim/list?status=active',
{ headers: { 'Authorization': 'Bearer rsk_your_api_key' } }
);
const { esims } = await res.json();
for (const esim of esims) {
// Un-throttle
await fetch(
`https://citrusmobile.com/api/v2/reseller/esim/${esim.iccid}/throttle`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ speed: 'NO_LIMIT' }),
}
);
// Snapshot current wallet as today's new starting point
await db.unlimitedPlans.update(
{ iccid: esim.iccid },
{ day_start_baseline_usd: esim.wallet_balance_usd }
);
}
console.log(`Reset ${esims.length} eSIMs to full speed`);
}// Schedule 30 days out from provision
setTimeout(async () => {
await fetch(
`https://citrusmobile.com/api/v2/reseller/esim/${esim.iccid}/terminate`,
{
method: 'POST',
headers: { 'Authorization': 'Bearer rsk_your_api_key' },
}
);
}, 30 * 24 * 60 * 60 * 1000); // 30 days
// Also listen for esim.balance_depleted — if the customer drains their $50
// ceiling before 30 days are up, the network will cut the SIM off and fire the
// event. You can either terminate early or offer them a top-up.Economics: At 512 Kbps throttled speed, a user can consume ~5 MB/hour max. Even if they stay connected 24/7 after the cap, that's only ~120 MB/day of throttled usage — costing you pennies at the rates returned by /rates. Your worst case on any single SIM is the fund ceiling you set in step 1, full stop.
Country-Specific eSIMs
Sell eSIMs targeted at specific travel destinations with localized pricing.
Build a storefront where customers pick their destination country, see the per-GB rate, and purchase. The eSIM works globally but you display country-specific pricing.
// Fetch all rates (cache for 1 hour)
const res = await fetch('https://citrusmobile.com/api/v2/reseller/rates', {
headers: { 'Authorization': 'Bearer rsk_your_api_key' },
});
const { countries } = await res.json();
// Build your product catalog with your markup
const YOUR_MARKUP = 1.4; // 40% margin
const catalog = countries
.filter(c => c.has_data)
.map(c => ({
name: c.name,
flag: c.flag,
iso2: c.iso2,
cost_per_gb: c.cheapest_per_gb_usd,
your_retail_per_gb: +(c.cheapest_per_gb_usd * YOUR_MARKUP).toFixed(2),
networks: c.networks,
}));
// Example: Japan → $1.38/GB cost → your price $1.93/GB// For your "Japan eSIM" page:
const res = await fetch(
'https://citrusmobile.com/api/v2/reseller/rates?country=JP',
{ headers: { 'Authorization': 'Bearer rsk_your_api_key' } }
);
const { countries } = await res.json();
const japan = countries[0];
// japan.networks → [{ operator: "KDDI Corporation", per_gb_usd: 1.38 }, ...]// Customer bought your "Japan - 5GB" product for $27.90 (5 GB × $5.58)
// You want to fund the SIM with $7.50 so 5 GB at $1.38/GB (Japan KDDI) lands
// comfortably inside the wallet. Your margin is $27.90 - $7.50 = $20.40.
const esim = await fetch('https://citrusmobile.com/api/v2/reseller/esim/provision', {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
end_user_reference: `order_${orderId}`,
label: `${customerName} - Japan`,
}),
}).then(r => r.json());
// Fund the SIM — without this step the SIM has a $0 wallet and
// cannot pass any data. The eSIM works in Japan AND everywhere else,
// at the per-country rates returned by /rates.
await fetch(`https://citrusmobile.com/api/v2/reseller/esim/${esim.iccid}/fund`, {
method: 'POST',
headers: {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 7.50 }),
});Travel eSIM Bundles (Groups)
Provision eSIMs for tour groups, corporate trips, or event attendees.
Batch-provision eSIMs for a group of travelers and track each one by name or employee ID.
const travelers = [
{ name: 'Alice Chen', email: 'alice@acme.com', id: 'EMP-001' },
{ name: 'Bob Mueller', email: 'bob@acme.com', id: 'EMP-002' },
{ name: 'Carlos Silva', email: 'carlos@acme.com', id: 'EMP-003' },
];
const API = 'https://citrusmobile.com/api/v2/reseller';
const headers = {
'Authorization': 'Bearer rsk_your_api_key',
'Content-Type': 'application/json',
};
// How much to put on each traveler's SIM for the whole trip
const PER_TRAVELER_FUND_USD = 18; // rounds cleanly with no drift
// Provision AND fund one eSIM per traveler
const results = [];
for (const traveler of travelers) {
// 1. Provision
const provRes = await fetch(`${API}/esim/provision`, {
method: 'POST',
headers,
body: JSON.stringify({
end_user_reference: traveler.id,
label: `${traveler.name} - Berlin Trip`,
}),
});
const esim = await provRes.json();
// 2. Fund — without this, the SIM has $0 wallet and won't pass data
await fetch(`${API}/esim/${esim.iccid}/fund`, {
method: 'POST',
headers,
body: JSON.stringify({ amount: PER_TRAVELER_FUND_USD }),
});
results.push({ ...traveler, iccid: esim.iccid, qr: esim.qr_code });
}
// Email each traveler their QR code
for (const r of results) {
await sendEmail({
to: r.email,
subject: `Your ${r.name.split(' ')[0]}-trip eSIM`,
qrCode: r.qr,
});
}// Check activation status of all eSIMs
const res = await fetch(`${API}/esim/list?status=active`, { headers });
const { esims } = await res.json();
console.log(`${esims.length} of ${travelers.length} eSIMs activated`);
// After the trip: terminate all
for (const esim of esims) {
await fetch(`${API}/esim/${esim.iccid}/terminate`, {
method: 'POST', headers
});
}White-Label eSIM Storefront
Build a fully branded eSIM website. Your customers never see Citrus Mobile.
Integrate the API into your checkout flow and deliver eSIMs under your own brand.
const express = require('express');
const app = express();
const CITRUS_API = 'https://citrusmobile.com/api/v2/reseller';
const API_KEY = process.env.CITRUS_API_KEY; // rsk_...
// POST /checkout — called after customer pays on your site
app.post('/checkout', async (req, res) => {
const { customer_email, customer_name, order_id, plan_fund_usd } = req.body;
// 1. Provision eSIM via Citrus API
const esimRes = await fetch(`${CITRUS_API}/esim/provision`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
end_user_reference: order_id,
label: customer_name,
}),
});
const esim = await esimRes.json();
// 2. Fund the SIM from your reseller balance. plan_fund_usd is whatever
// portion of what the customer paid you want to credit to the SIM —
// anything less than what you charged is your margin. WITHOUT this step
// the SIM has a $0 wallet and your customer can't use any data.
await fetch(`${CITRUS_API}/esim/${esim.iccid}/fund`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: plan_fund_usd }),
});
// 3. Save to your database
await db.orders.create({
order_id,
customer_email,
iccid: esim.iccid,
qr_code: esim.qr_code,
status: 'delivered',
});
// 4. Send branded email with QR code
await sendBrandedEmail({
to: customer_email,
subject: 'Your eSIM from YourBrand',
qr_code: esim.qr_code,
install_url: esim.direct_install_url,
});
res.json({ success: true, iccid: esim.iccid });
});
// GET /dashboard/:order_id — customer checks their eSIM status
app.get('/dashboard/:order_id', async (req, res) => {
const order = await db.orders.findOne({ order_id: req.params.order_id });
const statusRes = await fetch(
`${CITRUS_API}/esim/${order.iccid}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const status = await statusRes.json();
res.json({ status: status.status, activated: !!status.activated_at });
});// Webhook endpoint — Citrus notifies you when eSIM activates
app.post('/webhooks/citrus', (req, res) => {
// Verify signature (see Webhooks section in the API Reference)
const event = req.body;
if (event.event === 'esim.activated') {
// Update your customer's dashboard
db.orders.update(
{ iccid: event.data.iccid },
{ status: 'active', activated_at: event.data.activated_at }
);
// Optionally notify your customer
sendPushNotification(customer, 'Your eSIM is now active!');
}
res.sendStatus(200);
});IoT / M2M Connectivity
Connect devices — GPS trackers, kiosks, sensors, vending machines.
Provision eSIMs for devices, control connectivity remotely, and throttle to save costs on low-bandwidth devices.
// GPS trackers only need ~50 KB per location ping. A $9 fund typically
// lasts months at low-usage IoT workloads — pick whatever matches your
// expected burn rate. ($9 rounds cleanly with zero display drift.)
const esim = await citrus.post('/esim/provision', {
label: 'Tracker-Fleet-042',
end_user_reference: 'vehicle_plate_ABC123',
});
// Fund the SIM — without this the device can't connect at all.
await fetch(`${API}/esim/${esim.iccid}/fund`, {
method: 'POST',
headers,
body: JSON.stringify({ amount: 9 }),
});
// Immediately throttle to minimum speed (saves on per-session data)
await fetch(`${API}/esim/${esim.iccid}/throttle`, {
method: 'POST',
headers,
body: JSON.stringify({ speed: 'SPEED_100_KBPS' }),
});
// Subscribe to esim.balance_low so you can top the tracker up before
// it hits zero and goes offline:
// POST /webhooks { events: ['esim.balance_low', 'esim.balance_depleted'] }// Disable all devices overnight to save data costs
async function disableFleetOvernight() {
const { esims } = await fetch(`${API}/esim/list?status=active`, { headers })
.then(r => r.json());
for (const esim of esims) {
await fetch(`${API}/esim/${esim.iccid}/disable`, {
method: 'POST', headers
});
}
}
// Re-enable in the morning
async function enableFleetMorning() {
const { esims } = await fetch(`${API}/esim/list`, { headers })
.then(r => r.json());
for (const esim of esims.filter(e => e.status === 'active' || e.status === 'pending')) {
await fetch(`${API}/esim/${esim.iccid}/enable`, {
method: 'POST', headers
});
}
}// Register webhook to get alerted before your fleet goes offline
await fetch(`${API}/webhooks`, {
method: 'POST',
headers,
body: JSON.stringify({
url: 'https://your-server.com/webhooks/citrus',
events: ['balance.low', 'balance.depleted'],
}),
});
// In your webhook handler:
app.post('/webhooks/citrus', (req, res) => {
if (req.body.event === 'balance.low') {
// Auto-top-up or alert ops team
alertOpsTeam(`Citrus balance low: $${req.body.data.balance_usd}`);
}
res.sendStatus(200);
});Ready to go deeper?
See every endpoint with request/response schemas and try it live.
Open API Reference