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.
Generate your credentials
Send both headers on every request
X-Merchant-Id and X-Api-Key. There is no login call and no bearer token — these two headers are the whole of authentication.Check they work before anything else
orderPrefix — the string every order id from your account starts with.List your catalogue
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.Place a test order first
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.
https://partners-api.mobisim.comResponse 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": {}
}| Field | What it holds |
|---|---|
success | The field to branch on. Present on every response including the 401 from authentication. |
message | Null on success; the reason on failure. Written for a human reading a log — branch on the HTTP status, not on this text. |
data | The 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. |
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.
| Header | Description | Required |
|---|---|---|
X-Merchant-Id | Your company's public identifier — 32 hex characters. Safe to log. Issued under Settings → Developers. | required |
X-Api-Key | The 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>'
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.
The package is checked against your catalogue
Your wallet is checked
The eSIM is ordered from the provider
The order is recorded and your wallet debited
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.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.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.
Buy, not TopUp.Check the eSIM can take one
canTopUp, so you do not have to cross-reference the catalogue. The same fact appears as hasTopUp on a package.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.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.It is recorded as its own order
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.iccid of the one you mean. You will find it on the order as esim.iccid.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.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.
Generate a reference before you call
Retry with the same reference, not a new one
replayed: true and the original order underneath. Nothing is bought and nothing is charged.Use it to read the order back
companyOrderId, so it is also what you pass to GET /api/Order/GetOrderDetails/{orderId}. You never have to store an id we generated.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 varies | What to expect |
|---|---|
| Profile delivery | Most 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 method | A 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 usage | Reported 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 status | lifecycle 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. |
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.
Add your endpoint
whsec_…) to verify deliveries with.Answer 2xx quickly
Verify the signature
Send a test event
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.
| Type | Sent when |
|---|---|
order.created | An order was placed. data.esims lists each eSIM as completed or provisioning. |
order.completed | One 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.completed | Data was confirmed on an existing eSIM. A top-up answered pending: true sends this once the provider confirms it. |
refund.completed | A refund was accepted and your wallet credited, less the processing fee. |
wallet.low_balance | Your wallet fell to or below the alert threshold set in Settings. At most once a day per threshold. |
esim.installed | The 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.activated | The 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_threshold | An 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.updated | Your 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_scheduled | A 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.expiring | An 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.test | You 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"
}
}| Field | What it holds |
|---|---|
id | Unique per event and identical on every retry. Store the ones you have processed and skip repeats — delivery is at least once. |
data.orderId | The order's companyOrderId — your orderPrefix followed by the clientReference you sent. Match on it, or pass it to GET /api/Order/GetOrderDetails/{orderId}. |
data.activationCode | The LPA string. Render it as a QR code for your customer. Fields a plan does not issue are null, never omitted. |
amount, currency | What 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
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.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.
| Status | Meaning |
|---|---|
401 | Headers missing, credentials wrong, or your company's API access is switched off. The message says which. |
400 | The request was understood but refused — insufficient balance, a package not in your catalogue, or the provider rejecting the order. Nothing was charged. |
404 | No such package or order for your company. A package another company can sell reads as absent to you. |