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
Provision eSIMs for tour groups, corporate trips, or event attendees.
Batch-provision eSIMs for a party of travelers and track each one by name or employee ID. Each traveler gets their own wallet, which is what you want when they are each paying for their own trip.
If instead they are all spending from one company budget, do not fund them one by one. Use Always-On Team Data and give them a single shared balance that refills itself.
const travelers = [
{ name: 'Alice Chen', email: '[email protected]', id: 'EMP-001' },
{ name: 'Bob Mueller', email: '[email protected]', id: 'EMP-002' },
{ name: 'Carlos Silva', email: '[email protected]', 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