v1.0.0REST · JSON · HTTPS

Mobisim Public API

Sell eSIMs from your own storefront. Browse the catalogue assigned to your company, buy packages at your negotiated prices, and read back each order's eSIM profile and live data usage — over plain REST with JSON payloads.

Every request is authenticated with a merchant key pair and is scoped to one company: you only ever see your own catalogue, orders and wallet. There is no login step and no token to refresh.

Getting Started

Follow these steps to make your first API call.

1

Generate your credentials

In the Partner Portal, open Settings → Developers and generate an API key. You get a Merchant ID (your company's public identifier) and an API Key (the secret). Regenerating rotates the key and leaves the Merchant ID unchanged.
2

Send both headers on every request

Add X-Merchant-Id and X-Api-Key. There is no login call and no bearer token — these two headers are the whole of authentication.
3

Check they work before anything else

GET /api/Account/Me answers “are my keys right” without placing an order or interpreting a 401. It names the account and returns your balance, catalogue size, and orderPrefix — the string every order id from your account starts with.
4

List your catalogue

Call GET /api/Packages/GetPackages. Each entry's packageData.id is the packageId you buy with, and price is your wholesale cost — retailPrice is only a suggestion. To load the whole catalogue with each plan's networks in one request, add ?all=true&includeDetails=true instead of paging and calling GetPackageDetails for every package — send Accept-Encoding: gzip and cache the result, since it is a large response that changes far less often than you read it.
5

Place a test order first

POST /api/Purchase/TestBuy exercises the whole flow — including the confirmation email — without contacting a provider or charging your wallet. Switch to Buy once it looks right, and send a clientReference with it from the very first call.

Base URL

All API endpoints are relative to this base URL.

Basehttps://partners-api.mobisim.com

Response shape

Every endpoint returns the same envelope, on success and on failure alike. Write your unwrapping and your error handling once.

{
  "success": true,
  "message": null,
  "data": { ... },      // or [ ... ], or null on failure
  "totalCount": 1274,
  "page": 1,
  "pageSize": 50,
  "totalPages": 26,
  "stats": {}
}
FieldWhat it holds
successThe field to branch on. Present on every response including the 401 from authentication.
messageNull on success; the reason on failure. Written for a human reading a log — branch on the HTTP status, not on this text.
dataThe payload: an object for a single resource, an array for a listing, null on failure.
totalCount …Real numbers on a listing, zeroes on a single object or an error. There is a next page while page < totalPages.
⚠
This replaced five different shapes across the API, so it is a breaking change if you integrated before it shipped. Order/GetAllOrders returned a bare array and Order/GetOrderDetails a bare object — both now sit under data. Wallet/GetWalletTransactions moved its paging counts up out of data onto the envelope, and Order/List dropped hasNext and hasPrevious, both derivable from page and totalPages.

Authentication

Every request under /api must carry both headers below. Together they identify your company and authorise the request; the documentation pages themselves are open.

HeaderDescriptionRequired
X-Merchant-IdYour company's public identifier — 32 hex characters. Safe to log. Issued under Settings → Developers.required
X-Api-KeyThe secret issued alongside it. Treat it like a password. Rotate it from the same page — the Merchant ID stays the same, so rotating does not break your integration's identity.required
curl 'https://partners-api.mobisim.com/api/Packages/GetPackages?page=1&pageSize=10' \
  -H 'X-Merchant-Id: <your merchant id>' \
  -H 'X-Api-Key: <your api key>'
⚠
Keep your API key server-side. Never commit it or embed it in a browser, mobile app, or anything else a customer can read — the key can spend your wallet balance. Rotate it immediately if it is ever exposed.
ℹ
No bearer token. This API has no login, session, or refresh flow. If a request comes back 401, the two headers are missing, misspelled, or the key has been rotated.

Buying an eSIM

What happens when you call POST /api/Purchase/Buy.

1

The package is checked against your catalogue

It must be assigned to your company, visible, active, and still offered by the provider. Anything else is rejected before an order is placed.
2

Your wallet is checked

You need cost price × quantity available. If the balance is short, the request fails and nothing is ordered.
3

The eSIM is ordered from the provider

One provider call per unit. If the provider rejects the order, the request fails and your wallet is not charged.
4

The order is recorded and your wallet debited

You get back a companyOrderId per unit, plus the eSIM profile. The confirmation email to the customer is sent in the background, so a slow mail server never delays the response.
✓
Use TestBuy while you build. It runs the same validation and sends a real confirmation email, but never contacts a provider and never charges your wallet — the response tells you what a real purchase would have cost.
ℹ
Send sendEmail: false on Buy (or TestBuy) to skip the confirmation email — useful if you deliver the eSIM and QR code to the customer yourself. email is still required either way; it stays on the order and TSim orders register it upstream regardless of whether a confirmation goes out. Trigger it later with POST /api/Purchase/SendEmail/{orderId}, which sends one confirmation per unit on the order.

Topping up an eSIM

Adding data to an eSIM you have already sold, rather than issuing a new one.

⚠
A top-up issues no new QR code and no new activation code. The data attaches to the profile your customer already installed, so there is nothing to send them and nothing for them to do on their device. If you are expecting something to forward on, you want Buy, not TopUp.
1

Check the eSIM can take one

Not every plan supports it — the provider decides. GET /api/Order/GetOrderDetails/{orderId} answers with canTopUp, so you do not have to cross-reference the catalogue. The same fact appears as hasTopUp on a package.
2

Ask which packages you may apply

GET /api/Purchase/TopUpOptions?orderId=... — or ?iccid=.... It returns the same package shape as the catalogue, priced for you and paged the same way. Eligibility depends on the eSIM as much as on the package, so ask rather than filtering the catalogue yourself: anything returned here will be accepted, and nothing is offered that would then be refused.
3

Apply it

POST /api/Purchase/TopUp with the packageId and either orderId or iccid. Your wallet is debited only after the provider confirms the data actually reached the eSIM — a refused top-up costs you nothing.
4

It is recorded as its own order

You get a new companyOrderId. The original order is left untouched, so the top-up has its own price, its own id and its own line in your history rather than quietly amending a past sale.
ℹ
An order that covers several eSIMs is ambiguous on its own — the request is refused rather than guessing which traveller to top up. Send the iccid of the one you mean. You will find it on the order as esim.iccid.
ℹ
Top-ups appear in GET /api/Order/List alongside sales, each marked orderType: "TopUp" and carrying the iccid the data went onto. Filter with ?type=Sale or ?type=TopUp. Read the field before counting rows as eSIMs — a top-up sold data, not a device.
✓
Occasionally the provider accepts a top-up without confirming it in time. That answers pending: true — the order is real and paid for, so do not retry it. Read the order back a little later and the added allowance will be there.

Idempotency & retries

A purchase that times out on your side has usually still completed here — the eSIM was issued and your wallet was debited. The response just never made it back. Retrying blind buys a second eSIM and charges you again.

Send a clientReference on every Buy and TopUp and that stops being a problem. It is your own id for the order — unique per order on your side. If a request carrying one you have used before arrives, the original order is returned instead of a new one being placed.

1

Generate a reference before you call

Letters, digits, dashes and underscores, up to 64 characters. Your own order id works well. A reference with anything else in it is rejected rather than quietly rewritten — a rewritten one would no longer match the one you hold, which defeats the point.
2

Retry with the same reference, not a new one

The replay comes back with replayed: true and the original order underneath. Nothing is bought and nothing is charged.
3

Use it to read the order back

Your reference becomes the order's companyOrderId, so it is also what you pass to GET /api/Order/GetOrderDetails/{orderId}. You never have to store an id we generated.
⚠
The check reads before it acts, so two genuinely simultaneous requests carrying the same reference can both get through. It covers the retry that actually happens — seconds or minutes later — and is not a substitute for not firing the same order twice at once.
ℹ
One reference identifies exactly one order, whatever kind it is. Reusing the reference you bought an eSIM under to top that same eSIM up is refused, naming the order it already belongs to — a top-up is a separate order and needs a reference of its own. Nothing is charged by the refusal.

Fulfilment

Plans differ in how quickly a profile is issued and how much the network reports back afterwards. You never choose this — it is a property of the package — but it changes what GET /api/Order/GetOrderDetails can tell you, so handle both cases.

What variesWhat to expect
Profile deliveryMost plans return the profile on the Buy response. Some are issued asynchronously, and the order reports Provisioning until it appears — wait for the order.completed webhook or poll GetOrderDetails, rather than treating it as a failure.
Install methodA QR code and activation code are always present. PIN/PUK codes and direct install links appear only on plans that issue them, and are null otherwise.
Live usageReported for most plans. Where it is not available, usedDataGb and remainingDataGb are null. The figure comes from the network, which only updates it every few hours — on some carriers once a day — so polling more often than that returns the same number. We hold network reads for a short while (up to ten minutes for usage) to keep repeat calls from reaching the network at all.
Install statuslifecycle says whether the customer has downloaded and installed the eSIM, and on which phone where the network reports it. It is null on plans whose network does not report installs — read that as “unknown”, not “not installed”. To be told instead of asking, subscribe to the esim.installed webhook.
ℹ
A null usage figure means “not reported”, not “nothing used”. Do not render it to customers as 0 GB consumed.

Merchant Wallet

Your company holds one wallet. Every purchase debits it by the cost price — what Mobisim charges you — not the retail price you resell at. A purchase is refused outright if the balance will not cover it, so keep an eye on the balance before a large batch.

Two prices per package

price is what you are charged. retailPrice is your own selling price — it is recorded on the order and shown on the customer's receipt, but never billed to you.

Balance & transactions

Read both from the Wallet endpoints. Each purchase writes a matching Sell transaction.

⚠
PUT /api/Wallet/AddWalletBalance credits your wallet directly. It takes effect immediately with no payment step, so anyone holding your API key can spend and top up without limit. This is another reason to keep the key server-side.

Webhooks

Instead of polling, give us a URL and we POST a signed JSON event to it the moment something happens: an eSIM profile is ready, a top-up lands, a refund is credited, your wallet runs low. Events cover every order on your account, whether placed through this API or the Partner Portal.

1

Add your endpoint

In the Partner Portal, open Developers and add a public HTTPS URL. Choose the events you want, or all of them — including types added later. You get a signing secret (whsec_…) to verify deliveries with.
2

Answer 2xx quickly

Respond with any 2xx within 10 seconds, then do the work. Anything else — a timeout, a 4xx, a 5xx, a redirect — counts as a failure. Redirects are not followed.
3

Verify the signature

Reject any request whose signature does not match, or whose timestamp is more than five minutes old. See the example below.
4

Send a test event

The Send test event button posts a webhook.test event and shows exactly how your endpoint answered. Every delivery, with its payload and your response, is kept in the portal's delivery log for 30 days and can be resent.

Events

esim.installed, esim.activated, esim.usage_threshold, esim.expiring, catalog.updated and catalog.price_scheduled are delivered only to an endpoint that names them — an endpoint set to receive “all events” does not get these six. Tick them on the Developers page when you want them.

TypeSent when
order.createdAn order was placed. data.esims lists each eSIM as completed or provisioning.
order.completedOne eSIM's profile is ready to install — at purchase, or later for plans that provision asynchronously. Carries the activation code. One event per eSIM.
topup.completedData was confirmed on an existing eSIM. A top-up answered pending: true sends this once the provider confirms it.
refund.completedA refund was accepted and your wallet credited, less the processing fee.
wallet.low_balanceYour wallet fell to or below the alert threshold set in Settings. At most once a day per threshold.
esim.installedThe customer installed the eSIM on a phone. data.device and data.eid are set where the plan's network reports them. Fires once per eSIM, straight from the network's own notice, so there is no need to poll GetOrderDetails to find out. Only on plans whose network reports installs.
esim.activatedThe plan started counting: the customer first used the eSIM. data.activatedAt is the start and data.expiresAt the end, once the network has said. Fires once per eSIM. Only on plans whose network reports activation.
esim.usage_thresholdAn eSIM passed 80%, then 100%, of its data. data.threshold says which mark — switch on it. Each fires once per eSIM, and an eSIM that passes both between checks receives both. Only on plans whose provider reports usage.
catalog.updatedYour catalogue changed: prices moved, packages were added, or packages were withdrawn. Sent once a day, with the same changes as the daily catalogue email, at your own prices. data.priceChanges lists each package with oldPrice, newPrice and changePercent, largest moves first; data.added and data.removed list the rest. Each list holds at most 100 entries — the counts (priceChangeCount, addedCount, removedCount) are always complete, and data.truncated is true when a list was cut short, in which case read GetPackages?all=true for the rest. Prices are your wholesale price, the same figure as price in GetPackages.
catalog.price_scheduledA price increase is coming. Sent the first time we see it, before it applies to you, and only while a notice period is in force. data.changes lists each package with oldPrice (what you pay now, and keep paying until the date), newPrice and effectiveAt, soonest first; the list holds at most 100 entries, with data.count always the full number and data.truncated true when it was cut short. When the date arrives the change appears in catalog.updated like any other. The same list is on the Price changes page in the Partner Portal.
esim.expiringAn eSIM is 3 days, then 1 day, from expiring. data.daysRemaining says which mark. data.canTopUp says whether the plan accepts a renewal — call Purchase/TopUpOptions for the compatible packages.
webhook.testYou pressed Send test event. Always delivered, whatever you subscribed to.

Request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: MobiSIM-Webhooks/1.0
X-MobiSim-Event-Id: evt_4f1c2b9a0e5d4c7fa3b8e6d2c1a09f7e
X-MobiSim-Event-Type: order.completed
X-MobiSim-Timestamp: 1789466400
X-MobiSim-Signature: v1=5b2e0c…

{
  "id": "evt_4f1c2b9a0e5d4c7fa3b8e6d2c1a09f7e",
  "type": "order.completed",
  "createdAt": "2026-09-13T10:00:00Z",
  "data": {
    "orderId": "ACME-your-reference-123",
    "packageId": 1842,
    "packageTitle": "Europe 10GB 30 days",
    "iccid": "8944500000000000000",
    "activationCode": "LPA:1$smdp.example.com$ABCD-1234",
    "iosInstallLink": null,
    "androidInstallLink": null,
    "apn": "internet"
  }
}
FieldWhat it holds
idUnique per event and identical on every retry. Store the ones you have processed and skip repeats — delivery is at least once.
data.orderIdThe order's companyOrderId — your orderPrefix followed by the clientReference you sent. Match on it, or pass it to GET /api/Order/GetOrderDetails/{orderId}.
data.activationCodeThe LPA string. Render it as a QR code for your customer. Fields a plan does not issue are null, never omitted.
amount, currencyWhat your wallet was charged or credited, in EUR.

Verifying signatures

Compute HMAC-SHA256 over {X-MobiSim-Timestamp}.{raw body} using the whole signing secret as the key, hex-encode it, and compare it with the X-MobiSim-Signature header after v1=. Use the raw bytes you received — re-serialising parsed JSON changes them.

// Node.js (Express): app.post("/webhooks/mobisim", express.raw({ type: "application/json" }), handler)
import crypto from "node:crypto";

function verify(req, secret) {
  const timestamp = req.header("X-MobiSim-Timestamp");
  const signature = req.header("X-MobiSim-Signature") ?? "";

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = "v1=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${req.body.toString("utf8")}`)
    .digest("hex");

  return signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
# Python
import hmac, hashlib, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers["X-MobiSim-Timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = "v1=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, headers.get("X-MobiSim-Signature", ""))

Retries and ordering

ℹ
A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours — 8 attempts over about two days. After that it is marked failed, and you can resend it from the delivery log.
⚠
Events can arrive out of order and, rarely, more than once. An order.completed can reach you before its order.created. Deduplicate on id, and treat GET /api/Order/GetOrderDetails as the source of truth when the order of events matters.
✓
Rotating the signing secret takes effect immediately, so deploy the new secret to your receiver straight after rotating. Pausing the endpoint stops events being recorded at all, so unpausing does not replay what you missed — use GetOrderDetails to catch up.
ℹ
TestBuy orders do not send webhook events. Use Send test event to exercise your receiver.

Errors

Failures come back in the same envelope as everything else, with success: false, a human-readable message, and data: null.

StatusMeaning
401Headers missing, credentials wrong, or your company's API access is switched off. The message says which.
400The request was understood but refused — insufficient balance, a package not in your catalogue, or the provider rejecting the order. Nothing was charged.
404No such package or order for your company. A package another company can sell reads as absent to you.