Firsty BuildersGuidesHow to provision your first eSIM via API in 30 minutes
Integration

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.

VVVince VissersMay 25, 2026· 12 min read
Integration

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:

  1. Authenticates with OAuth2 client credentials
  2. Orders an eSIM (creates the profile and eSIM in one call)
  3. Attaches a data package
  4. Generates a scannable QR code

At the end, you'll have a

sim-qr.png
file you can scan with your phone to install a real test eSIM. You'll see a SIM appear in your phone's settings, on Firsty's test network.

Prerequisites

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:

axios
for HTTP requests and
qrcode
for generating the QR image.

Step 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:

  • client_id
    : your 6-digit client reference (e.g.,
    123456
    )
  • client_secret
    : your secret (e.g.,
    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:

bash
mkdir firsty-first-esim
cd firsty-first-esim
npm init -y
npm install axios qrcode dotenv

Create an

index.js
file. We'll build it up step by step.

javascript
// 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.

javascript
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;
}

A few things worth knowing.

The token is valid for 24 hours (

expires_in: 86400
in the response). In a real production app, you'd cache this token in memory or Redis, check the expiry before each call, and refresh proactively a few minutes before it expires. Fetching a fresh token on every API call is wasteful and you'll hit the auth rate limit (10 requests per 15 minutes). We're skipping that for now to keep the example readable, but it's the single most common mistake we see in production integrations.

The token endpoint expects

application/x-www-form-urlencoded
body, not JSON. Easy to miss. The
URLSearchParams
constructor in Node.js handles the encoding for you.

If the credentials are wrong, you get a

401 Unauthorized
with
Invalid client credentials
in the body. If you're missing a required field, you get
400 Bad Request
.

Step 4: Order the eSIM

A single call to

POST /esims
creates a profile AND an eSIM together, returning everything you need to install it on a device.

javascript
async 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

status
and
lifecycleStatus
fields.
released
means the SM-DP+ profile is available for download (the QR code is ready).
preactive
means the IMSI is provisioned but not yet active on the network. Both transition once the customer installs the eSIM.

Step 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/packages
. For this tutorial, use a placeholder
C123456XYZDUSR
and replace it with a real plan reference from the catalog in your account.

javascript
async 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

packageReference
(23-character ID), the activation timestamp, expiration, and recurring flag.

Why 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

activationCode
you got back is in LPA format. LPA stands for Local Profile Assistant, the GSMA standard for how phones download eSIM profiles. The format looks like this:

LPA:1$provider.example.com$ACTIVATION_CODE_HERE

Three parts separated by dollar signs:

  • LPA:1
    : the LPA protocol version
  • provider.example.com
    : the SM-DP+ server address (the carrier's provisioning server)
  • ACTIVATION_CODE_HERE
    : the matching ID that identifies your specific profile

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.

javascript
async 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:

javascript
require('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:

bash
node index.js

You should see four log lines and a

sim-qr.png
file in your project directory. Open the PNG, point your phone's camera at it, and tap the eSIM installation prompt that appears.

A 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-Key
header on all POST endpoints (PATCH too). Same key within 48 hours returns the cached response. Use it.

Error 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.

ShareLinkedInX
New guides every Tuesday. No marketing nonsense.