How to provision your first eSIM via API in 30 minutes
From OAuth token to first eSIM activated, with QR code generation server-side. Real code, real credentials, real eSIM, in about 30 minutes.
How to provision your first eSIM via API in 30 minutes
If you've integrated a payments API before, eSIM provisioning will feel familiar: get a token, make a call, handle the response. The difference is that most eSIM providers gate testing behind a sales call and weeks of paperwork. Firsty doesn't. You can be holding a working eSIM 30 minutes from now.
This guide takes you from signup to a scannable QR code. We'll use Node.js because the example code stays compact, but the same pattern works in any language that can speak HTTP and parse JSON.
The code in this post is real. The credentials work against Firsty's staging API. The eSIM you provision at the end will activate on your phone.
What you'll build
A Node.js script that:
- Authenticates with OAuth2 client credentials
- Orders an eSIM (creates the profile and eSIM in one call)
- Attaches a data package
- Generates a scannable QR code
At the end, you'll have a
sim-qr.pngPrerequisites
Try it yourself
Free sandbox. Real Tier-1 carriers. 60 seconds from signup to credentials.
Get started →You need:
- Node.js 18+ installed
- Firsty sandbox credentials (we'll get these in step 1)
- A modern phone that supports eSIM (iPhone XS or later, Samsung Galaxy S20 or later, recent Google Pixel)
- About 30 minutes
We'll install two npm packages:
axiosqrcodeStep 1: Get sandbox credentials
Head to builders.firsty.app, configure your sandbox, and verify your email with the one-time code. The credentials modal will show you two values:
- : your 6-digit client reference (e.g.,
client_id)123456 - : your secret (e.g.,
client_secret)sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The sandbox is free. You won't be billed for test SIMs, calls, or anything else you do in sandbox mode. The credentials work against the same real Firsty API that production uses, just pointed at the staging environment.
Save the credentials as environment variables. Don't hardcode them, and don't commit them to git.
bash# .env FIRSTY_CLIENT_ID=123456 FIRSTY_CLIENT_SECRET=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Step 2: Set up your project
Create a fresh Node.js project:
bashmkdir firsty-first-esim cd firsty-first-esim npm init -y npm install axios qrcode dotenv
Create an
index.jsjavascript// index.js require('dotenv').config(); const axios = require('axios'); const QRCode = require('qrcode'); const BASE_URL = 'https://connect.test.firsty.app/api/v3';
That's all the setup. Now let's authenticate.
Step 3: Get an access token
Firsty uses OAuth2 with the client credentials grant, the standard pattern for server-to-server API access. You POST your credentials to the token endpoint, you get back a JWT access token valid for 24 hours, and you include that token as a Bearer header on every subsequent request.
javascriptasync function getToken() { const response = await axios.post( `${BASE_URL}/auth/token`, new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.FIRSTY_CLIENT_ID, client_secret: process.env.FIRSTY_CLIENT_SECRET, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, } ); return response.data.access_token; }
A few things worth knowing.
The token is valid for 24 hours (
expires_in: 86400The token endpoint expects
application/x-www-form-urlencodedURLSearchParamsIf the credentials are wrong, you get a
401 UnauthorizedInvalid client credentials400 Bad RequestStep 4: Order the eSIM
A single call to
POST /esimsjavascriptasync function orderEsim(token) { const response = await axios.post( `${BASE_URL}/esims`, {}, { headers: { Authorization: `Bearer ${token}` }, } ); return response.data.data; }
The response looks like this:
json{ "data": { "profileReference": "1234567890123456", "esimReference": "123456000000000001", "iccid": "89012345678901234567", "activationCode": "LPA:1$provider.example.com$ACTIVATION_CODE_HERE", "status": "released", "lifecycleStatus": "preactive", "createdAt": "2026-05-25T10:00:00Z" } }
You have an eSIM ready to install. But it has no data package attached yet. Your customer can install it, but it won't connect to anything. That's step two.
Note the
statuslifecycleStatusreleasedpreactiveStep 5: Attach a data package
To make the eSIM functional, you order a package against it. Each package is created from a plan (the blueprint) and represents a specific subscription instance.
Plan references are 14-character opaque tokens. You'll get the list of available plans via
GET /catalog/packagesC123456XYZDUSRjavascriptasync function orderPackage(token, profileReference, esimReference, planRef) { const response = await axios.post( `${BASE_URL}/profiles/${profileReference}/esims/${esimReference}/packages`, { planReference: planRef }, { headers: { Authorization: `Bearer ${token}` }, } ); return response.data.data; }
The endpoint returns 200 with details of the package created: a
packageReferenceWhy is this separate from eSIM creation?
You might wonder why we don't bundle "create eSIM with this plan" into one call. The short answer is flexibility.
Splitting eSIM creation from package ordering means you can:
- Pre-provision SIMs in bulk before you know which plan a customer wants (useful for travel apps where the customer picks a destination at the last minute)
- Retry the cheap operation (package ordering) without re-creating an expensive one (the eSIM)
- Cleanly handle the case where eSIM creation succeeded but package ordering failed
We've written about why this architecture matters in more detail if you're interested.
Step 6: Generate a QR code
The
activationCodeLPA:1$provider.example.com$ACTIVATION_CODE_HERE
Three parts separated by dollar signs:
- : the LPA protocol version
LPA:1 - : the SM-DP+ server address (the carrier's provisioning server)
provider.example.com - : the matching ID that identifies your specific profile
ACTIVATION_CODE_HERE
You don't show this string to your customer. You encode it as a QR code, the customer scans the QR with their phone's camera, and the phone's eSIM installer parses the LPA string and downloads the profile.
javascriptasync function generateQR(activationCode, filename = 'sim-qr.png') { await QRCode.toFile(filename, activationCode, { width: 400, margin: 2, color: { dark: '#000000', light: '#FFFFFF', }, }); console.log(`QR code saved to ${filename}`); }
Keep the QR on a clean white background with high contrast. Phone cameras struggle with stylized QRs at small sizes. For production use, render the QR server-side and serve it as a PNG. Don't trust client-side QR libraries with sensitive activation codes.
Putting it all together
Here's the complete script:
javascriptrequire('dotenv').config(); const axios = require('axios'); const QRCode = require('qrcode'); const BASE_URL = 'https://connect.test.firsty.app/api/v3'; async function getToken() { const response = await axios.post( `${BASE_URL}/auth/token`, new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.FIRSTY_CLIENT_ID, client_secret: process.env.FIRSTY_CLIENT_SECRET, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); return response.data.access_token; } async function orderEsim(token) { const response = await axios.post( `${BASE_URL}/esims`, {}, { headers: { Authorization: `Bearer ${token}` } } ); return response.data.data; } async function orderPackage(token, profileRef, esimRef, planRef) { const response = await axios.post( `${BASE_URL}/profiles/${profileRef}/esims/${esimRef}/packages`, { planReference: planRef }, { headers: { Authorization: `Bearer ${token}` } } ); return response.data.data; } async function generateQR(activationCode, filename = 'sim-qr.png') { await QRCode.toFile(filename, activationCode, { width: 400, margin: 2 }); console.log(`QR saved to ${filename}`); } (async () => { try { const token = await getToken(); console.log('Got access token'); const esim = await orderEsim(token); console.log('eSIM ordered:', esim.profileReference); await orderPackage( token, esim.profileReference, esim.esimReference, 'C123456XYZDUSR' ); console.log('Package attached'); await generateQR(esim.activationCode); console.log('Done. Scan sim-qr.png with your phone.'); } catch (err) { console.error('Error:', err.response?.data || err.message); } })();
Run it:
bashnode index.js
You should see four log lines and a
sim-qr.pngA few seconds later, you'll have a test eSIM on your phone. It won't actually move data (it's a sandbox eSIM, non-billable), but it will install and appear in your phone's cellular settings.
What we left out (on purpose)
This is the happy path. Real production code needs more.
Token caching. Don't fetch a fresh token on every API call. The auth endpoint is rate-limited to 10 requests per 15 minutes. Cache it with its expiry, refresh proactively. The biggest source of production bugs we see is mid-batch token expiry. We wrote about OAuth2 client credentials in production: the patterns that work and the ones that bite you.
Idempotency keys. What happens if your network blips between sending the request and receiving the response? Did the SIM get provisioned or not? Without idempotency keys, you'll occasionally double-provision when you retry. Firsty supports the
X-Idempotency-KeyError handling. Carriers go down. Networks fail. Your code should distinguish between retriable errors (5xx, network timeouts) and permanent ones (4xx). A naive retry loop will get you rate-limited.
Rate limits. General endpoints are limited to 1000 requests per 15 minutes. If you're provisioning in bulk, you need exponential backoff and a queue. We've got a whole post on bulk-provisioning eSIMs without hitting rate limits.
What's next
You've provisioned an eSIM end-to-end via API. From here, the path depends on what you're building:
- Adding eSIM to an existing app? Wrap this code in an API route in your backend, expose a "buy connectivity" button in your app, and use the customer's userId as a foreign key linking to the eSIM you provisioned.
- Building a travel eSIM marketplace? Add country selection logic, plan pricing, Stripe checkout. We're covering this in a dedicated guide.
- Reselling at scale? You'll need bulk operations, customer dashboards, billing reconciliation. Talk to us about the Carrier plan.
The sandbox is free to keep using. Provision as many test eSIMs as you need. When you're ready to go live, the pricing page shows the path from sandbox to production.
Questions or stuck somewhere? Reach us at builders@firsty.app or grab time on a call.
Related guides
OAuth2 client credentials in production: what most tutorials get wrong
Token caching, refresh strategy, and the security mistakes we see in production integrations every week.
The LPA activation code format explained
What the LPA format actually is, how to render QR codes from it correctly, and why some QR codes work on iPhone but not Android.
profileReference vs esimReference vs ICCID: which identifier when?
Three identifiers, three meanings, frequently confused. Here is which one to use where.