MeiSIM Dealer API
Real T-Mobile numbers, owned and managed in the US by MEISIM USA LLC, a Wyoming company run from Los Angeles.
REST API for authorized dealers to programmatically order eSIMs, top up SIMs, and manage their wallet.
| Base URL | https://api.meisimusa.com |
|---|---|
| Content type | application/json |
| API version | v1 |
Quick start
# 1. Get your API key (one-time)
curl -X POST https://api.meisimusa.com/dealer/send-otp \
-H 'content-type: application/json' \
-d '{"email":"you@yourcompany.com"}'
# Check email for the 6-digit code, then:
curl -X POST https://api.meisimusa.com/dealer/verify-otp \
-H 'content-type: application/json' \
-d '{"email":"you@yourcompany.com","code":"123456"}'
# → returns { "verified": true, "apiKey": "..." }
# 2. Use the API key on every request
curl https://api.meisimusa.com/api/v1/me \
-H 'x-dealer-key: YOUR_API_KEY'
Public catalogue — no key required
Reading the plan catalogue needs no dealer key and no signup:
GET https://api.meisimusa.com/mm/products.
It is documented in full on the public page — Catalogue API →
(endpoint, filters, response shape and fair-use limits).
Authentication
Everything below this point is the dealer API and does need a key. Two equivalent auth methods. Use either on any endpoint.
Option A — API Key (recommended for backend integration)
Issued via the OTP flow below. Passed in the request header:
x-dealer-key: YOUR_API_KEY
The key does not expire. Treat it like a password.
Option B — JWT (for portal/UI integrations)
Issued via the password-login flow. Passed in the standard Bearer header:
Authorization: Bearer YOUR_JWT
JWT expires after 12 hours. Re-login to refresh.
Auth flow — OTP login
POST/dealer/send-otp
Send a 6-digit code to your email.
{ "email": "you@yourcompany.com" }
Response: { "sent": true }. Rate limit: 5 sends per 15 min per IP. Code expires in 5 minutes; max 5 verification attempts.
POST/dealer/verify-otp
Verify the code and receive your API key.
{ "email": "you@yourcompany.com", "code": "123456" }
Response:
{ "verified": true, "apiKey": "dk_..." }
Rate limit: 10 verifications per 15 min per IP.
Auth flow — password login (returns JWT)
POST/dealer/auth/set-password
One-time password setup. Requires x-dealer-key from the OTP flow.
{
"email": "you@yourcompany.com",
"password": "min-8-chars"
}
Response: { "ok": true }.
POST/dealer/auth/login
Email + password → JWT.
{ "email": "you@yourcompany.com", "password": "..." }
Response:
{
"ok": true,
"token": "eyJhbGc...",
"expiresAt": "2026-05-06T12:00:00.000Z",
"dealer": { "id": "...", "email": "...", "company": "..." }
}
Rate limit: 8 attempts per 15 min per IP.
eSIM endpoints
GET/api/v1/esim/countries
List supported countries. Returns the upstream country catalog.
POST/api/v1/esim/bundles
List bundles for a country/region.
{
"countryISO": "JP", // optional, ISO 3166-1 alpha-2
"countryName": "Japan", // optional, alternative to ISO
"region": "Asia", // optional
"pageNumber": 1, // optional, default 1
"pageSize": 20 // optional, default 20, max 100
}
Response: paginated bundle list with each bundle's name, data allowance, validity, and price.
POST/api/v1/esim/order
Order an eSIM for an end customer.
{
"bundleName": "esim_5gb_30days_jp", // required, from /bundles
"customerReference": "ORDER-12345", // optional, your tracking id
"emailAddress": "end-customer@x.com", // optional, where the QR is sent
"qrToCustomer": true // optional, default false
}
Response: { "Status": "Success", "Iccid": "...", "QrCode": "...", ... } (upstream-format). The order is logged in your dealer history.
POST/api/v1/esim/details
Get activation status and data usage.
{ "iccid": "894900..." }
Returns activation state, data used vs allowance, expiry.
POST/api/v1/esim/revoke
Deactivate an eSIM upstream and update your dealer order history.
{ "iccid": "894900..." }
Rate limit: 10 per minute per dealer.
Top-up endpoints
For recharging existing SIMs (not new eSIMs).
GET/api/v1/topup/networks
List networks eligible for top-up.
POST/api/v1/topup/confirm
Validate a target SIM exists on a network. Use this before recharge to avoid charging a non-existent SIM.
{
"networkName": "Vodafone-UK",
"contactNumber": "+447700900000", // either contactNumber
"simSerialNumber": "894400..." // OR simSerialNumber
}
POST/api/v1/topup/recharge
Execute a top-up recharge.
{
"networkName": "Vodafone-UK", // required
"contactNumber": "+447700900000", // OR simSerialNumber
"simSerialNumber": "894400...",
"topUpValue": 20, // required, currency depends on network
"bundleValue": null, // optional bundle add-on
"bundleProductCode": null, // the plan to apply, from topup/confirm BundleList
"creditOnly": false // true = plain credit, no plan
}
topup/confirm returns two lists for a line: BundleList (plans, each with a BundleProductCode) and TopUpList (plain credit amounts).
- A plan: send its
bundleProductCode. The code must be one of the line's bundles. - Credit only: send
"creditOnly": true, no code, and atopUpValuethat is inTopUpList. - A line that has plans is never recharged with a missing code unless
creditOnlyis true. Credit without a plan is used at the carrier's pay-as-you-go rate, so it has to be asked for by name. - A line with no plans (for example O2) takes a
topUpValuefromTopUpListwith no code.
US Unlimited by the day
A prepaid US unlimited line (talk, text, 5G data, real US number) sold by the day and pushed to the customer's phone over the air; no QR code. Dealer price: $1.25 per day, any number of days from 1 to 365, no minimum (the website sells it in tiers; your account can carry its own day rate). The start day counts as day 1; service ends at midnight US Pacific on the last day. One line per order. Your wallet is debited when the order is accepted and refunded if the carrier refuses the activation.
POST/api/v1/daily-lines/order
{
"customerEmail": "traveler@example.com",
"customerName": "Ana Lima",
"days": 10,
"start_date": "2026-11-02", // optional: today (US Pacific) when omitted; up to 90 days ahead
"eid": "89049032...5307", // the phone's 32-digit eSIM id (or "imei": 15 digits); one is required
"clientRef": "your-ref-123" // optional, idempotent: the same ref returns the original order
}
Response 201: order_id, short_id, order_state (scheduled for a later start, activating while the carrier works, fulfilled when the line is on), charged_usd, phone once known, and fd with days, start_date, end_date, ends_at. Poll GET /api/v1/orders or use your order webhook; the number appears when the carrier confirms, usually within minutes.
Extending a running line
Send "extend_order": "<short_id of the earlier order>" with days instead of eid/start_date. The new days start the day after the current last day on the same number; no new activation, no gap. Only a line that is active can be extended; a closed line needs a new order.
GET/api/v1/daily-lines
Your lines: phone, status (activating, active, closed, failed), days, start_date, end_date, ends_at, customer_email, order_id.
Account & history
GET/api/v1/me
Whoami.
{ "id": "...", "company_name": "...", "email": "...", "status": "approved" }
GET/api/v1/orders
Recent orders. Returns up to the last 100, newest first.
[
{
"id": "...",
"order_type": "esim" | "topup",
"bundle_name": "esim_5gb_30days_jp",
"customer_reference": "ORDER-12345",
"email_address": "end-customer@x.com",
"iccid": "894900...",
"amount": 6.50,
"status": "completed",
"created_at": "2026-05-05T12:34:56Z"
}
]
Wallet
GET/dealer/wallet
Wallet balance and recent transactions.
{
"ok": true,
"balance": 245.30,
"markupPct": 15,
"resendFee": 1.00,
"company": "...",
"email": "...",
"transactions": [
{
"id": "...",
"created_at": "2026-05-05T12:34:56Z",
"type": "debit" | "topup",
"amount_usd": -6.50,
"balance_after_usd": 245.30,
"description": "...",
"stripe_session_id": null,
"related_order_id": "..."
}
]
}
GET/dealer/statement
CSV export. Default range: last 90 days.
GET /dealer/statement?from=2026-04-01&to=2026-05-01
Returns CSV with columns: Date, Type, Amount USD, Balance after USD, Description, Order ID, Stripe session.
POST/dealer/topup
Create Stripe checkout session for wallet top-up.
{ "amountUsd": 100 }
amountUsd must be between $50 and $10,000. Response: { ok, checkoutUrl, netAmt, grossAmt, fee }. Redirect user to checkoutUrl. Rate limit: 5/min.
POST/dealer/topup-intent
Create Stripe PaymentIntent for embedded checkout.
{ "amountUsd": 100 }
Response: { ok, clientSecret, publishableKey, netAmt, grossAmt, fee }. Use Stripe.js with clientSecret. Rate limit: 10/min.
GET/dealer/topup/preview?net=100
Preview Stripe fee. Response: { ok, net, gross, fee }.
Order from your wallet
POST/dealer/order
Alternative to /api/v1/esim/order — debits your wallet at your dealer price.
Moxee plans need imei but no eid. The carrier issues the Moxee eSIM profile from its own pool, so there is no customer EID to bind — send one and it is ignored. The customer's 15-digit device IMEI is required. Every other US prepaid (p3:) carrier still requires both.
Changed 2026-08-10 — breaking. Moxee previously needed neither field, because we activated it onto a SIM from our own stock. Moxee orders sent without an imei now fail with a 400.
{
"productId": "p3:att-prepaid-30", // required
"customerEmail": "end@x.com", // required
"customerName": "Jane Doe", // required
"quantity": 1, // optional, 1–20
"language": "en", // optional
"imei": "359123456789012", // required for ALL US prepaid, Moxee included
"eid": "89001012...", // required for US prepaid — NOT for Moxee
"sim_type": "physical", // optional, Moxee only: activate the SIM cards we sent you
"iccids": ["8901260812345678901"], // with sim_type physical: one ICCID per line (= quantity)
"whatsappNumber":"+13055550123", // optional — see below
"address": { // optional — see below
"first_name": "Jane",
"last_name": "Doe",
"address_line_1": "123 Main St",
"city": "Phoenix",
"state": "AZ",
"zip_code": "85001",
"phone": "+15555550100"
}
}
Physical SIM cards (Moxee only)
If we have sent you Moxee SIM cards, activate a plan onto a card instead of an eSIM: send "sim_type": "physical" and "iccids", one ICCID per line, so the array length equals quantity. The imei is optional for a card (it goes into whichever handset you choose); given, it must still be 15 digits. No eid.
The order's lines come back with "sim_type": "physical", the number in phone_number, the card's ICCID, and qr_available: false: put the card in the handset and the number is live on it. A card activates once; an ICCID already on a live or pending order is refused with a 409 naming that order. The same works in bulk upload through an iccid column (one card per row, quantity 1).
Carriers your account cannot activate
Some US carriers can be switched off for dealer accounts — across the board or for one account — usually because the carrier's own fraud team has raised something about traffic on those lines. When that applies to you, the carrier's plans do not appear in the catalogue you are served, and /dealer/order (and /dealer/replace/:orderId) answers 403 with "<Carrier> activations are not available on your account." Nothing is charged and no line is created. Refills of lines you already sold on that carrier keep working — only new activations are refused.
2026-08-12 — LinkUp Mobile is blocked for all dealer accounts. AT&T's fraud department flagged LinkUp lines activated through us as SMS spamming. Existing LinkUp lines are unaffected and can still be recharged.
WhatsApp delivery (whatsappNumber)
Optional. When set, your customer also gets the assigned number, the QR and the one-tap install links on WhatsApp, the moment the line goes live. Email is sent either way — this adds a channel, it never replaces one.
- Include the country code —
+1 305 555 0123,447700900123. Spaces and punctuation are ignored. A bare 10-digit number is assumed to be US; anything else without a country code is rejected, because guessing one would deliver a stranger your customer's eSIM. - Must be 8–15 digits including the country code. An unusable number is ignored, not an error — the order still completes and the eSIM still goes out by email.
- This is your customer's number. Your own alerts are separate and per-event — see
/dealer/notify-prefs. - Only send a number the customer asked you to use.
The activation address (US p3: plans)
address is optional, and it does two jobs at once: it is registered with the carrier as the line's E911 (emergency services) address, and the carrier picks the number's area code from the ZIP — so a Phoenix ZIP produces a Phoenix number. Omit address entirely and we assign both for you.
If you send any of address_line_1, city, state or zip_code, you must send all four, valid. A partial address can't be used for either job, so the order is rejected with 400 rather than quietly activating on our address (which would give your customer an unexpected area code).
address_line_1 | Must start with a street number (123 Main St). A PO Box or PMB/mailbox is never a valid E911 address. |
|---|---|
city | 2+ characters |
state | 2-letter code (AZ), not the full state name |
zip_code | 5 digits (ZIP+4 accepted, extra digits ignored) |
first_name / last_name / address_line_2 / phone are always optional, and an address carrying only names is fine — it just names the customer.
We do not pre-validate the address against the carrier's E911 database (no carrier exposes one to resellers) — it is checked at activation. If the carrier rejects it, we automatically retry once with our own address so the line still activates; the area code then follows ours, not the one you asked for.
Response:
{
"ok": true,
"orderId": "...",
"shortId": "MM-AB12CD",
"unitPriceUsd": 25.00,
"totalUsd": 25.00,
"newBalance": 220.30,
"activation": "delivered" // "delivered" | "pending" | "failed"
}
Returns 402 Payment Required if balance is insufficient.
Branch on activation, not on ok. ok: true means the order was accepted and priced — the carrier can still refuse the activation after that. pending means the carrier has it and hasn't confirmed yet; we deliver automatically when it does.
When activation is "failed" the response also carries a failure object — the carrier's verdict, what it means, and the one thing that changes the outcome on a retry. Show whatToDo to whoever is holding the handset:
{
"ok": true,
"shortId": "MeiDlr-5WTVYMEKJUH2",
"activation": "failed",
"failure": {
"reason": "The device is compatible for esim activation through IMEI2. Please use IMEI2 for esim activation.",
"meaning": "The phone is fine — we were given the IMEI for its physical SIM slot, not its eSIM.",
"whatToDo": "Dial *#06# on the handset. Dual-SIM phones list TWO IMEIs — use the SECOND one (IMEI2), which is the eSIM. Place the order again with that number and it should go through.",
"refunded": true
}
}
refunded: true means the charge is already back in your wallet and there is nothing to claim. The one verdict to not retry blindly is a carrier timeout — there we cannot tell whether a line was created, and whatToDo says so.
On a successful order this response carries no eSIM data — take the orderId (or shortId) and call the endpoint below to get the QR / activation code.
GET/dealer/order/:orderId
The order's eSIM: QR, activation code, ICCID and assigned US number. Accepts the UUID or the short id (MeiDlr-…), and is scoped to your own orders. One entry per line, so a quantity: 3 order returns three.
{
"ok": true,
"orderId": "…", "shortId": "MeiDlr-3NSXHFD8THR2",
"productId": "p3:318:698", "state": "fulfilled", "quantity": 1,
"customer": { "email": "end@x.com", "name": "Jane Doe" },
// on a failed order only — same shape as POST /dealer/order's `failure`,
// alongside the raw carrier string in `error`
// "failure": { "reason": "…", "meaning": "…", "whatToDo": "…" },
"lines": [{
"line": 1,
"status": "delivered",
"activation_code": "LPA:1$CUST-001-V4-PROD-ATL2.GDSB.NET$EAF6DA2AB…",
"smdp": "CUST-001-V4-PROD-ATL2.GDSB.NET",
"matching_id": "EAF6DA2AB…",
"iccid": "89049032000001000096650386218347",
"phone_number": "3415128315",
"qr_png_url": "https://api.meisimusa.com/qr?size=480&text=…",
"install_ios_url": "https://esimsetup.apple.com/…",
"install_android_url": "https://esimsetup.android.com/…"
}]
}
Data balance
GET /dealer/order/:orderId/usage returns the live balance of a travel-data eSIM per line: data_total_mb, data_used_mb, data_left_mb, expires_at, and status: active, not_installed (the QR has not been scanned), expired (installed, but the plan ended) or deleted (removed from the phone). ?line=N narrows a multi-eSIM order. UK lines and US carrier lines answer 409 NO_USAGE_API: those networks expose no usage endpoint; the customer sees usage in the carrier's own app.
About the QR code
The carrier issues an activation string, not a picture. activation_code is the GSMA LPA:1$<smdp>$<matchingId> payload, and a QR code is simply that string encoded. This holds for LinkUp Mobile and every other US carrier. Three ways to deliver it:
- render
activation_codeas a QR yourself; - show
qr_png_url— a PNG we render, no auth needed, safe to drop straight into an<img>; - or call
GET /dealer/order/:orderId/qrfor the PNG bytes over your API key —?line=N(default 1) and?size=(64–1024, default 480).
install_ios_url / install_android_url are one-tap install links: on iOS 17.4+ and recent Android, tapping installs the eSIM with no scan at all.
status is one of three, and your next move differs for each:
delivered | we hold the line. Check qr_available before showing a QR |
|---|---|
provisioning | the carrier hasn't issued it yet — poll, don't treat as an error |
unavailable | order finished with nothing recorded. Won't change on its own — contact us |
Gate your QR rendering on qr_available, not on status. qr_source says where the QR comes from, and it matters:
activation_code | we hold the LPA string. qr_png_url always renders, and the one-tap install links work. |
|---|---|
carrier | the carrier holds the profile and serves its QR image (Moxee, LinkUp). qr_png_url fetches it on demand — handle a failed image load, since an older line may have no profile behind it. activation_code and the one-tap links are null; the customer scans the QR. |
The QR route answers 409 when there is no QR to give, and 404 only when the line does not exist.
Cancelled or refunded orders return no install data (lines: [], and the QR route answers 410) — their profiles are revoked at the carrier.
POST/dealer/resend-email/:orderId
Resend an eSIM activation email. Charges the resend fee (~$1).
{ "email": "alternate@x.com" }
Body is optional — omit email to resend to the original address.
POST/dealer/replace/:orderId
Issue a replacement eSIM at the original product's current price. Charged from your wallet. Returns 402 if insufficient balance.
Real T-Mobile numbers: incoming SMS and voice over SIP
Every endpoint here takes your API key. Numbers are closed per account by default; ask us to open them, and to add voice.
GET/dealer/sms-lines/offer
What you may buy and at what price: first_month_usd, renew_price_usd, term_days, your balance, and voice.available with voice.price_usd, the Voice + SMS price per number per month, when voice is open on your account.
POST/api/v1/sms/order
{ "count": 5, "area_code": "718", "zip_code": "11201", "voice": true, "billing": "monthly", "months": 2, "line_history": "fresh", "auto_renew": true, "customer_reference": "ord-48213" }
Which lines: line_history is required on monthly orders. "fresh" = a number you have never had, bought new from the carrier, never from our returned stock: 2 months minimum paid in advance whatever your account's usual minimum, and no refund if the carrier blocks it. "any" = a number from our stock, which may be one you returned before. Daily-rate orders are always "any"; a fresh line cannot be bought on the daily rate.
count is required, a whole number of numbers. A body without it, or with a field not listed here, is refused with 400 invalid_input and nothing is charged. A created order answers 201; a repeated customer_reference answers 200 with the first order.
Two ways to pay. billing: "monthly" (the default) pays months up front, 2 months minimum (2 to 12; an account we have allowed single months may send 1, and min_months on the offer says which you are; with no months sent the order is for your minimum): the first month at first_month_usd, every month after at renew_price_usd. billing: "daily" is the daily rate (daily.price_usd, or daily.voice_price_usd with voice). Every activation is daily.min_days days minimum, paid at purchase; from the day after, each morning we take the next day from your wallet. A new number has a one-time activation fee (daily.activation_fee_usd, or daily.voice_activation_fee_usd); a number that is already active in our stock has none, daily.ready_now says how many those are, and the response itemises it (days_paid, activation_fee_usd, new_numbers, from_pool). Returning a number early does not refund the days paid. If the wallet is short, the number stops at 22:00 US Eastern that day and is released; we retry at 18:00 US Eastern first.
count is required. area_code (3 digits) or zip_code (5 digits) asks the carrier for a number local to it; it is a request, not a promise. voice: true orders Voice + SMS lines at your Voice + SMS price, and needs voice open on your account. auto_renew: true switches auto-renew on for the numbers as they arrive. Charged to your wallet up front; 402 with shortfall when it is short. The numbers appear in /api/v1/sms/lines within a few minutes.
Your reference, and retries. Send customer_reference (up to 120 characters: letters, digits and . _ - : / #) to tie the order to your own records. It is unique on your account: sending the same reference again orders and charges nothing and answers { "ok": true, "duplicate": true, "order": {...} } with the first order. So if a call times out on your side, repeat it with the same reference, or look the order up. The response carries order_id, and every number in /api/v1/sms/lines carries the order_id and customer_reference of the order that bought it.
Days up front on the daily rate. With billing: "daily", send days (from daily.min_days to 90) to pay that many days at purchase instead of the minimum. After them the wallet pays day by day as usual; to end a number on its paid date instead, set POST /api/v1/sms/lines/:number/daily-stop.
GET/api/v1/sms/orders/:id
One order, by order_id or by your customer_reference: status (provisioning, partially_fulfilled, fulfilled, closed_refunded, cancelled), ordered, delivered, outstanding, refunded_numbers, refunded_usd, refunded_at, the numbers delivered so far, billing, days_paid, charged_usd, created_at, completed_at. GET /api/v1/sms/orders?limit=50 lists your latest orders the same way.
If the carrier cannot fill an order. When an order has received no new number for 3 hours, it is closed at what it delivered and the missing numbers are refunded to your wallet automatically: their share of the days or months paid and their activation fee. The order then shows closed_refunded with refunded_numbers and refunded_usd, the refund is a line in your wallet history, and you get a notice. The numbers you did receive are unaffected.
{ "ok": true, "ordered": 5, "charged_usd": 80.00, "billing": "monthly", "months": 2, "per_line_usd": 16.00, "term_days": 60, "voice": true,
"requested": { "area_code": "718", "zip_code": null } }
GET/api/v1/sms/lines?limit=100&offset=0&q=
Your numbers, paged. Each carries number, e164, status, expires_at, auto_renew, renew_price_usd, voice and, on a voice line, sip_status (preparing while the number is being attached to your SIP login, usually within a day, then ready) and sip once ready (host, port, user, password, one call at a time, caller ID is the number). Loop while next_offset is present.
GET/api/v1/sms/sip
Your account's SIP login, the one every voice number of yours registers with. configured: false while it is being prepared.
Choosing the number a call goes out from. One login carries all your voice numbers, so add the header X-CALL-FROM: 9292839689 (the 10-digit number, no +1) to the outbound INVITE. The From header does not choose it; without X-CALL-FROM the call leaves from one of your numbers picked by the switch. Each line's sip.caller_id_header gives the exact header to send.
Outbound calls reach US numbers only. These lines cannot call international numbers; a call abroad is refused with 480.
GET/api/v1/sms/messages?number=&since=&limit=
Incoming texts on your numbers. Ask for messages since the last id you saw so nothing falls between two polls.
POST/api/v1/sms/lines/:number/send
{ "to": "2125551234", "message": "Your code is 4821" }
{ "to": "68953", "message": "HELP" } // a 5 or 6 digit short code works too
Sends a text from one of your numbers to a 10-digit US number or to a 5 or 6 digit short code. Sending is switched on per account on request, and each number has its own monthly allowance; the same limits apply here as in the console, so a refusal comes back as 429 with the reason. Spam of any kind closes the account.
GET / POST/api/v1/sms/webhook
Where we push each incoming text as it arrives: { "url": "https://…", "secret": "…" }. Deliveries carry an HMAC-SHA256 signature of the body under the secret.
POST/api/v1/sms/lines/:number/renew
{ "months": 1 }. Extends the number from its current end date, charged to your wallet at renew_price_usd. Renewing early costs no days.
POST/api/v1/sms/renew-all
{ "months": 1 }. Renews every live number on the account, one after the other, and stops at the first the wallet cannot cover: renewed, total, charged_usd, stopped_short, failed[]. Top up and call again for the rest.
POST/api/v1/sms/lines/:number/auto-renew and /api/v1/sms/auto-renew-all
{ "auto_renew": true }. With auto-renew on, the next term is taken from your wallet shortly before the number would stop; if the wallet is short on the day, the number stops and is released.
POST/api/v1/sms/lines/:number/billing
{ "billing": "daily" } moves a monthly number to the daily rate from the day after its paid month ends; nothing is charged until then. { "billing": "monthly", "months": 2 } moves a daily number back to monthly, 2 months minimum, charged now at renew_price_usd per month, starting the day after its last paid day. Renew and auto-renew refuse a daily number (409); switch it first.
POST/api/v1/sms/lines/:number/daily-stop
{ "stop": true } stops the daily charges: the number works through its last paid day (daily_paid_through) and stops at 22:00 US Eastern, then it is released. { "stop": false } resumes before then. Nothing is refunded; every paid day is delivered. Each line carries billing, daily_price_usd, daily_paid_through, daily_stop and daily_unpaid_since.
PATCH/api/v1/sms/lines/:number
{ "allowed_contacts": ["2125551234"] } binds the number to the people it may text: while the list is non-empty, a send to any other destination answers 403 with code: "CONTACT_NOT_ALLOWED", whichever way the send is made. Up to 10 ten-digit US numbers; null or [] clears it. Inbound is not affected: the number still receives from anyone. This is how a platform gives one companion or agent its own line and keeps it one-to-one with the user who pays for it. { "label": "Mia for user 8812" } names the number (60 characters). Only these two fields are writable; an unknown field is refused. The answer carries the number as GET /api/v1/sms/lines shows it, including allowed_contacts.
POST/api/v1/sms/lines/:number/cancel
Not available. Numbers cannot be returned or cancelled: they are prepaid and run for the time paid, and nothing is refunded. This call answers 409 with code: "NO_RETURNS" and tells you what to do instead: on a daily-rate number, stop its charges with /daily-stop and it works through its last paid day; on a monthly number, switch auto-renew off and it ends on its paid-through date.
A monthly number runs the months it was paid for (2 minimum) and then stops unless renewed or on auto-renew. A daily-rate number runs while each morning's charge goes through and stops at 22:00 US Eastern on its last paid day when it does not. Either way, a number that stops goes back to our pool and is no longer yours. Its messages are kept for 48 hours in case you order the same number again (it comes back with them); after that they are erased and the number may be sold to anyone. Nothing is closed at the carrier.
Errors
Every error is one JSON shape. error is the sentence for a person. code is stable, so a program can branch on it. request_id is the same value as the x-request-id response header on every request: quote it to support and we find the call in our logs. retryable says whether trying again can help (a rate limit, a fault on our side) or cannot (a bad request, an empty wallet, nothing to find).
{ "error": "count is required: how many numbers, a whole number from 1 to 100.", "code": "invalid_input", "request_id": "iad1:iad1::k7f2x-1760300000000-9c1d2e3f4a5b", "retryable": false }
Codes: invalid_input (400), unauthorized (401), payment_required (402), forbidden (403), not_found (404), conflict (409), rate_limited (429), server_error (5xx).
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or malformed fields |
| 401 | Unauthorized — missing or invalid x-dealer-key / JWT |
| 402 | Payment required — insufficient wallet balance |
| 403 | Forbidden — account not approved, on hold, or suspended, or the carrier is not available on your account. If the message says your account is on hold, we have a question out to you by email: answer it and we will lift the hold. Nothing on the account is lost while it is held. |
| 404 | Not found — order, product, or dealer doesn't exist |
| 429 | Rate limit exceeded |
| 500 | Server error — try again, contact support if persistent |
Rate limits (per dealer unless noted)
| Endpoint group | Limit |
|---|---|
Order operations (/api/v1/esim/order, /api/v1/topup/recharge, /dealer/order) | No limit — order as many as your wallet balance covers |
Read operations (/api/v1/me, /api/v1/orders, /api/v1/esim/countries, /api/v1/esim/bundles, /api/v1/esim/details, /api/v1/topup/networks, /api/v1/topup/confirm, /api/v1/esim/revoke) | 600 / min |
Wallet top-ups (/dealer/topup, /dealer/topup-intent) | 5–10 / min |
Number orders (/api/v1/sms/order) | 6 / min |
Renew all (/api/v1/sms/renew-all) | 3 / min |
| OTP send | 5 / 15 min per IP |
| OTP verify | 10 / 15 min per IP |
| Password login | 8 / 15 min per IP |
Rate-limit window is sliding; on 429, wait the indicated period and retry.
eSIM Verification — Dealer API
Submit a batch of LPAs (eSIM activation codes) and we'll classify each as one of:
available, used, invalid, error, unknown.
The check is read-only — LPAs are never consumed by the verification.
Batches up to 50,000 rows. Results typically arrive within minutes.
Pricing: $1 per LPA, debited from your dealer wallet at submission time.
Rows that return error are refunded automatically — you only pay for successful verdicts.
Authentication uses either your bearer JWT (from /dealer/login) or the legacy x-dealer-key header.
POST /dealer/esim-verify — Submit batch
POST https://api.meisimusa.com/dealer/esim-verify
Authorization: Bearer <dealer-jwt> # OR: x-dealer-key: <your-key>
Content-Type: application/json
{
"lpas": [
"LPA:1$T-MOBILE.IDEMIA.IO$AYU36-O48VE-8PWDE-ZRXGS",
"LPA:1$T-MOBILE.IDEMIA.IO$O0VQX-7MJVW-99Y97-KLSI4",
"..."
],
"notify_email": "results@yourcompany.com" // optional
}
Response (200):
{
"ok": true,
"batch_id": "<uuid>",
"total_rows": 2,
"charged_usd": 2.00,
"wallet_balance_usd": 148.00
}
Errors:
| HTTP | Code | Meaning |
|---|---|---|
| 400 | No LPAs provided | Empty lpas array |
| 402 | INSUFFICIENT_FUNDS | Wallet balance below required total — response.balance and response.required show details |
| 403 | — | Dealer account not active |
GET /dealer/esim-verify/:batch_id — Poll batch
GET https://api.meisimusa.com/dealer/esim-verify/<batch-uuid>
Authorization: Bearer <dealer-jwt>
200 OK
{
"ok": true,
"batch": {
"id": "<uuid>",
"label": "Dealer Acme Inc · 100 LPAs",
"total_rows": 100,
"started_at": "2026-05-13T18:00:00Z",
"completed_at": null,
"paid_total_usd": 100.00
},
"progress": {
"total": 100,
"pending": 40,
"in_progress": 5,
"used": 35,
"available": 18,
"invalid": 1,
"error_count": 1,
"unknown": 0
}
}
GET /dealer/esim-verify/:batch_id/results.csv — Download CSV
Returns a CSV with columns iccid, lpa, status, reason, checked_at.
Safe to call while the batch is still running — pending rows show status=pending.
curl -H "Authorization: Bearer $DEALER_JWT" \
https://api.meisimusa.com/dealer/esim-verify/<batch-uuid>/results.csv \
-o results.csv
eSIM Supply — Portal API
Authentication is JWT bearer (not the x-dealer-key header above).
POST to /api/portal/v1/auth/login with the credentials we issued you to
receive a token. All endpoints in this section share the prefix
/api/portal/v1. Reseller portal access is by invitation only —
contact support@meisimusa.com.
eSIM Supply sells pre-validated AT&T eSIM stock. Each purchase atomically claims rows
from inventory using FOR UPDATE SKIP LOCKED, so two concurrent buyers never
collide on the same iccid. Wallet is debited per unit at your tier's price.
Before any purchase, read the eSIM Supply Terms of Service — in particular the rule that removing an eSIM as a troubleshooting step permanently destroys it and disqualifies it from replacement.
POST /services/esim_supply_att/execute — Buy eSIMs
Atomically claims quantity rows from inventory and assigns them to the caller.
POST /api/portal/v1/services/esim_supply_att/execute
Authorization: Bearer <jwt>
Content-Type: application/json
{
"sku_id": "<sku-uuid-from-/services/esim_supply_att/skus>",
"quantity": 10
}
Response (200):
{
"usage_id": 12345,
"vendor_order_ref": "INV-12345",
"cost_usd": 40.00,
"status": "succeeded",
"result": {
"carrier": "att",
"plan_type": "qr",
"quantity_ordered": 10,
"quantity_filled": 10,
"partial": false,
"esims": [
{ "iccid": "8901410...", "lpa": "LPA:1$...", "inventory_id": "...", "assigned_at": "..." },
...
]
}
}
If stock is short, quantity_filled < quantity_ordered, partial
is true, and the wallet is automatically credited for the shortfall.
If stock is zero, the call fails with VENDOR_ERROR and the full debit is refunded.
GET /esim-supply/stock — Stock count
Returns available counts so your UI can hide "Buy 1000" when only 12 are left.
GET /api/portal/v1/esim-supply/stock
Authorization: Bearer <jwt>
200 OK
{ "stats": { "att.qr.available": 12345, "att.qr.sold": 678, "att.qr.burned": 23 } }
GET /esim-supply/my — My eSIMs
Lists eSIMs assigned to the caller, newest first. Paged via limit and offset.
GET /api/portal/v1/esim-supply/my?limit=100&offset=0
Authorization: Bearer <jwt>
200 OK
{
"esims": [
{
"id": "...",
"iccid": "8901410...",
"lpa": "LPA:1$...",
"carrier": "att",
"plan_type": "qr",
"status": "sold",
"assigned_at": "2026-05-12T10:00:00Z",
"sold_via_usage_id": 12345
}
],
"total": 42,
"limit": 100,
"offset": 0
}
POST /esim-supply/replace — Request replacement
Submit an iccid you believe is broken. We enqueue a validator check on the matching LPA,
respond synchronously with a check_id, and you poll /esim-supply/replace/status/:check_id
until decision is not pending.
POST /api/portal/v1/esim-supply/replace
Authorization: Bearer <jwt>
{ "iccid": "8901410..." }
200 OK
{ "ok": true, "decision": "submitted", "check_id": "<uuid>", "message": "..." }
GET /api/portal/v1/esim-supply/replace/status/:check_id
Authorization: Bearer <jwt>
200 OK (one of)
{ "decision": "pending", "validator_status": "in_progress" }
{ "decision": "install_again", "validator_status": "available", "message": "..." }
{ "decision": "replaced", "validator_status": "used", "replacement": { "iccid": "...", "lpa": "...", "inventory_id": "..." } }
{ "decision": "awaiting_restock", "validator_status": "used", "message": "..." }
{ "decision": "admin_review", "validator_status": "error", "message": "..." }
Replacement policy: only validator-confirmed used verdicts auto-replace
from current inventory. available means the eSIM is still valid — try installing again.
Anything else routes to admin review within 24h.
eSIM Validator — Portal API
Submit a CSV of LPAs and we run each through a real eSIM-capable device on our worker
pool. Each row returns one of: available, used, invalid,
error, unknown. Median throughput is ~5s per LPA.
Pricing is per-row at the price shown in /services/esim_validate/skus.
The wallet is debited up-front for cost_per_row × rows; rows that error
out are refunded automatically — you only pay for successful verdicts.
POST /services/esim_validate/execute — Submit batch
POST /api/portal/v1/services/esim_validate/execute
Authorization: Bearer <jwt>
Content-Type: application/json
{
"sku_id": "<sku-uuid>",
"label": "att-batch-may12",
"csv": "iccid,lpa\n8901410...,LPA:1$...\n..."
}
Response (200) — batch is queued; status is executing until workers finish.
{
"usage_id": 54321,
"vendor_order_ref": "<batch-uuid>",
"cost_usd": 150.00,
"status": "executing",
"result": {
"batch_id": "<batch-uuid>",
"total_rows": 1000,
"cost_usd": 150.00,
"per_row_usd":0.15,
"message": "Batch queued. Poll /esim-check/customer/batches/:id for progress."
}
}
GET /esim-check/customer/batches/:id — Poll batch
GET /api/portal/v1/esim-check/customer/batches/<batch-uuid>
Authorization: Bearer <jwt>
200 OK
{
"batch": {
"id": "<uuid>",
"label": "att-batch-may12",
"total_rows": 1000,
"started_at": "...",
"completed_at": null,
"paid_total_usd": 150.00
},
"progress": {
"total": 1000,
"pending": 400,
"in_progress": 8,
"used": 350,
"available": 230,
"invalid": 8,
"error_count": 2,
"unknown": 2
}
}
GET /esim-check/customer/batches/:id/results.csv — Download results
Returns a CSV with columns iccid, lpa, status, reason, checked_at.
Safe to call while the batch is still running — pending rows show status=pending.
curl -H "Authorization: Bearer $TOKEN" \
https://api.meisimusa.com/api/portal/v1/esim-check/customer/batches/<batch-uuid>/results.csv \
-o results.csv
Code samples
JavaScript / Node.js
const API = 'https://api.meisimusa.com';
const KEY = process.env.MEISIM_DEALER_KEY;
async function orderEsim(bundleName, customerEmail) {
const r = await fetch(`${API}/api/v1/esim/order`, {
method: 'POST',
headers: {
'x-dealer-key': KEY,
'content-type': 'application/json',
},
body: JSON.stringify({
bundleName,
emailAddress: customerEmail,
qrToCustomer: true,
}),
});
if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
return r.json();
}
Python
import os, requests
API = 'https://api.meisimusa.com'
KEY = os.environ['MEISIM_DEALER_KEY']
def order_esim(bundle_name, customer_email):
r = requests.post(
f'{API}/api/v1/esim/order',
headers={'x-dealer-key': KEY},
json={
'bundleName': bundle_name,
'emailAddress': customer_email,
'qrToCustomer': True,
},
timeout=30,
)
r.raise_for_status()
return r.json()
curl
curl -X POST https://api.meisimusa.com/api/v1/esim/order \
-H "x-dealer-key: $MEISIM_DEALER_KEY" \
-H 'content-type: application/json' \
-d '{"bundleName":"esim_5gb_30days_jp","emailAddress":"end@x.com","qrToCustomer":true}'
For dealer onboarding, account questions, or technical support, contact your account manager directly.
Document version: v1.0 — 2026-05-05