Reseller API v1
API documentation
One base URL: https://afriesim.net/api/v1/partner. JSON in, JSON out. Bearer key auth. Test keys use a sandbox, live keys spend your prepaid wallet.
Authentication
Every call carries your key in the Authorization header. Keys start with af_test_ (sandbox) or af_live_ (real orders). Create and revoke keys in the portal under API keys. A key is shown once; we store only a hash.
curl -H "Authorization: Bearer af_test_YOUR_KEY" https://afriesim.net/api/v1/partner/account
Test keys answer from a sandbox: orders return made-up eSIMs that never install, nothing is charged, nothing reaches the network. Use them to build your integration. Live keys work once your account is approved, and spend your prepaid wallet.
Rate limit: 100 requests per minute per key. Above that you get 429 rate_limited with a Retry-After header. Twenty failed authentications from one address in a minute also return 429.
Send JSON bodies with Content-Type: application/json. Form encoded bodies work too. All money is in US dollars with two decimals.
Errors
Every error is a JSON object with one error key, an HTTP status that matches, a machine code and a plain sentence.
{
"error": {
"code": "unauthorized",
"message": "Missing or malformed API key. Send: Authorization: Bearer af_live_... or af_test_..."
}
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_partner_ref, invalid_plan, invalid_quantity, invalid_email, invalid_url, invalid_event, invalid_request, limit_reached, not_available_in_sandbox | Your request is wrong. The message says what to fix. |
| 401 | unauthorized | Missing, malformed, unknown or revoked key. |
| 402 | insufficient_balance | Wallet too low. balance and required are included. |
| 403 | account_not_live | Live key used before the account was approved. |
| 404 | not_found | No such order, eSIM, webhook or endpoint. |
| 409 | not_cancellable, not_refundable, ref_reused_after_failure | The action is not possible in the eSIM's current state. |
| 422 | plan_unavailable, number_terms_required, topup_unavailable | The plan or top-up cannot be sold right now. |
| 429 | rate_limited | Slow down. Retry after the number of seconds in Retry-After. |
| 500 | internal_error | Our fault. For an order, nothing was charged; retry with the same partner_ref. |
| 502 | provider_failure | The network refused. For an order, your wallet was refunded; retry later with a new partner_ref. |
{
"error": {
"code": "plan_unavailable",
"message": "Plan P999999 does not exist or is not currently available."
}
}
{
"error": {
"code": "insufficient_balance",
"message": "Wallet balance is too low for this order.",
"balance": 3.2,
"required": 18.0
}
}
GET /account
Who you are, your tier and discount, which key mode you used, your wallet balance and your webhook signing secret.
Example
curl -H "Authorization: Bearer af_live_YOUR_KEY" https://afriesim.net/api/v1/partner/account
Response (200):
{
"id": 1,
"company": "Demo Reseller Ltd",
"name": "Demo Admin",
"email": "support@afriesim.net",
"country": "France",
"website": "https://demo-reseller.example",
"tier": "starter",
"discount_pct": 12,
"price_floor": null,
"account_mode": "live",
"approved_at": "2026-11-06 16:06:42",
"key_mode": "live",
"key_label": "Handover live key",
"wallet_balance": 3.2,
"currency": "USD",
"webhook_secret": "02b3abb3...(48 hex chars)",
"created_at": "2026-11-06 16:04:08"
}
Catalogue
GET /catalogue/countries
Every country and region that has plans on sale, with the number of plans. Use code (country) or slug (region) to filter the plan list.
curl -H "Authorization: Bearer af_test_YOUR_KEY" https://afriesim.net/api/v1/partner/catalogue/countries
Response (200), shortened to the first entries:
{
"countries": [
{
"code": "AF",
"name": "Afghanistan",
"slug": "afghanistan",
"region": null,
"plans": 6
},
{
"code": "AX",
"name": "Aland Islands",
"slug": "aland-islands",
"region": null,
"plans": 11
},
{
"code": "AL",
"name": "Albania",
"slug": "albania",
"region": null,
"plans": 13
}
],
"regions": [
{
"slug": "africa",
"name": "Africa",
"country_count": 21,
"plans": 17
},
{
"slug": "asia-14",
"name": "Asia (14 countries)",
"country_count": 14,
"plans": 16
}
]
}
GET /catalogue/plans
Plans with the retail price and your price. Filter with ?country=NL (ISO code) or ?region=europe (slug). Without a filter you get every plan, about 3,300 rows, so cache it and refresh at most once an hour. plan_id is what you send to create an order: P plus a number for a country plan, R plus a number for a region plan. per_day true means the data figure is per day (a day pass). A plan with plan_kind "number" includes a phone number and carries a terms list you must accept when ordering.
curl -H "Authorization: Bearer af_test_YOUR_KEY" "https://afriesim.net/api/v1/partner/catalogue/plans?country=NL"
Response (200), shortened to two plans:
{
"discount_pct": 12,
"count": 20,
"plans": [
{
"plan_id": "P1465",
"country": "Netherlands",
"country_code": "NL",
"region": null,
"data_gb": 1,
"validity_days": 1,
"per_day": false,
"label": "1GB / 1 days",
"note": "",
"network": "3G/4G/5G",
"plan_kind": "data",
"retail_price": 2.05,
"your_price": 1.8,
"currency": "USD"
},
{
"plan_id": "P1468",
"country": "Netherlands",
"country_code": "NL",
"region": null,
"data_gb": 1,
"validity_days": 7,
"per_day": false,
"label": "1GB / 7 days",
"note": "",
"network": "3G/4G/5G",
"plan_kind": "data",
"retail_price": 2.05,
"your_price": 1.8,
"currency": "USD"
}
]
}
Orders
POST /orders
Buy one or more eSIMs of one plan. Fields:
| Field | Required | Meaning |
|---|---|---|
| plan_id | yes | From the catalogue, for example P1465 or R60. |
| partner_ref | yes | Your own unique reference, 1 to 100 characters (letters, digits, dot, dash, underscore). This is the idempotency key: sending the same partner_ref again returns the same order and never charges twice. |
| quantity | no | 1 to 10, default 1. Each unit is its own eSIM. |
| end_user_email | no | Your customer's email, stored on the order for your records. We do not email them. |
| accept_number_terms | no | Must be true when the plan includes a phone number. |
The live price is taken at the moment of the order and debited from your wallet before the network is asked. If the network declines, the unit is refunded at once and the response says so. If the network is slow, the unit comes back with status processing and empty credentials; fetch the order again within a few minutes or listen for the esim.ready webhook. A unit that never arrives is refunded automatically after 30 minutes.
Example (test key)
curl -X POST -H "Authorization: Bearer af_test_YOUR_KEY" -H "Content-Type: application/json" \\
-d '{"plan_id":"P1465","quantity":1,"partner_ref":"demo-ref-001","end_user_email":"traveller@example.com"}' \\
https://afriesim.net/api/v1/partner/ordersResponse (201):
{
"id": "test_1",
"order_number": "TEST-A001",
"partner_ref": "demo-ref-001",
"status": "completed",
"total": 1.8,
"currency": "USD",
"created_at": "2026-11-06 16:05:36",
"mode": "test",
"esims": [
{
"plan": "Netherlands 1GB / 1 days",
"country": "Netherlands",
"data_gb": 1,
"validity_days": 1,
"price": 1.8,
"iccid": "8988931820342767218",
"lpa": "LPA:1$sandbox.afriesim.net$MXTEST-A15E0ACDA3B6",
"qr_url": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=LPA%3A1%24sandbox.afriesim.net%24MXTEST-A15E0ACDA3B6",
"status": "ready",
"expires_at": null,
"note": "SANDBOX eSIM. Not a real profile, it will not install."
}
]
}
Same partner_ref sent again
curl -X POST ... -d '{"plan_id":"P1465","partner_ref":"demo-ref-001"}' https://afriesim.net/api/v1/partner/ordersResponse (200):
{
"id": "test_1",
"order_number": "TEST-A001",
"partner_ref": "demo-ref-001",
"status": "completed",
"total": 1.8,
"currency": "USD",
"created_at": "2026-11-06 16:05:36",
"mode": "test",
"esims": [
{
"plan": "Netherlands 1GB / 1 days",
"country": "Netherlands",
"data_gb": 1,
"validity_days": 1,
"price": 1.8,
"iccid": "8988931820342767218",
"lpa": "LPA:1$sandbox.afriesim.net$MXTEST-A15E0ACDA3B6",
"qr_url": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=LPA%3A1%24sandbox.afriesim.net%24MXTEST-A15E0ACDA3B6",
"status": "ready",
"expires_at": null,
"note": "SANDBOX eSIM. Not a real profile, it will not install."
}
],
"replayed": true
}
Note "replayed": true and the 200 instead of 201.
Example (live key, real eSIM)
curl -X POST -H "Authorization: Bearer af_live_YOUR_KEY" -H "Content-Type: application/json" \\
-d '{"plan_id":"P1465","quantity":1,"partner_ref":"handover-live-001"}' https://afriesim.net/api/v1/partner/ordersResponse (201):
{
"id": 4,
"order_number": "ORD-C202EA1F",
"partner_ref": "handover-live-001",
"status": "processing",
"total": 1.8,
"currency": "USD",
"end_user_email": "support@afriesim.net",
"created_at": "2026-11-06 16:10:47",
"mode": "live",
"esims": [
{
"item_id": 4,
"plan": "Netherlands 1GB / 1 days",
"country": "Netherlands",
"data_gb": 1,
"validity_days": 1,
"price": 1.8,
"iccid": "",
"lpa": "",
"qr_url": "",
"status": "processing",
"expires_at": null
}
],
"wallet_balance": 3.2,
"warning": "1 unit(s) are still being allocated by the provider; they complete automatically within minutes (watch the esim.ready webhook or re-fetch the order) or are refunded."
}
This real order came back still allocating. Fetching it two minutes later:
GET /orders/ORD-C202EA1F
curl -H "Authorization: Bearer af_live_YOUR_KEY" https://afriesim.net/api/v1/partner/orders/ORD-C202EA1F
Response (200):
{
"id": 4,
"order_number": "ORD-C202EA1F",
"partner_ref": "handover-live-001",
"status": "completed",
"total": 1.8,
"currency": "USD",
"end_user_email": "support@afriesim.net",
"created_at": "2026-11-06 16:10:47",
"mode": "live",
"esims": [
{
"item_id": 4,
"plan": "Netherlands 1GB / 1 days",
"country": "Netherlands",
"data_gb": 1,
"validity_days": 1,
"price": 1.8,
"iccid": "8910300000059801256",
"lpa": "LPA:1$rsp-eu.simlessly.com$F58626D950CC4F9A8D21B85CB9076D1F",
"qr_url": "https://p.qrsim.net/d945088eb2634c1d8c6b270698c8ef5f.png",
"status": "ready",
"expires_at": "2027-05-05 16:10:49"
}
]
}
Bad reference
curl -X POST ... -d '{"plan_id":"P1465","partner_ref":"bad ref!"}' https://afriesim.net/api/v1/partner/ordersResponse (400):
{
"error": {
"code": "invalid_partner_ref",
"message": "partner_ref is required: 1-100 chars, letters/digits/dot/dash/underscore. It is your idempotency key."
}
}
GET /orders
Your orders, newest first. ?limit=50&offset=0 to page. Each order includes its eSIMs. A test key lists sandbox orders, a live key lists real ones.
GET /orders/{id}
One order by its numeric id, order number (ORD-...) or your partner_ref. Sandbox orders use test_1 or TEST-A001.
eSIMs
GET /esims
Every eSIM you own, newest first, one line each.
{
"mode": "live",
"limit": 50,
"offset": 0,
"esims": [
{
"iccid": "8910300000059801256",
"order_number": "ORD-C202EA1F",
"partner_ref": "handover-live-001",
"plan": "Netherlands 1GB / 1 days",
"status": "ready",
"expires_at": "2027-05-05 16:10:49",
"created_at": "2026-11-06 16:10:49"
}
]
}
GET /esims/{iccid}
One eSIM with its order reference, activation code (LPA), QR image URL and status. Give the QR to your customer, or the LPA string for manual entry. Statuses: ready (not installed yet), installed, active, low_data, used_up, expired, suspended, cancelled, revoked, processing (still allocating), provisioning_failed (refunded).
{
"order_number": "ORD-C202EA1F",
"partner_ref": "handover-live-001",
"mode": "live",
"item_id": 4,
"plan": "Netherlands 1GB / 1 days",
"country": "Netherlands",
"data_gb": 1,
"validity_days": 1,
"price": 1.8,
"iccid": "8910300000059801256",
"lpa": "LPA:1$rsp-eu.simlessly.com$F58626D950CC4F9A8D21B85CB9076D1F",
"qr_url": "https://p.qrsim.net/d945088eb2634c1d8c6b270698c8ef5f.png",
"status": "ready",
"expires_at": "2027-05-05 16:10:49"
}
GET /esims/{iccid}/usage
Live data usage from the network. Sandbox eSIMs return a fixed figure.
Example (live)
curl -H "Authorization: Bearer af_live_YOUR_KEY" https://afriesim.net/api/v1/partner/esims/8910300000059801256/usage
Response (200):
{
"iccid": "8910300000059801256",
"status": "ready",
"data_total_mb": 1024,
"data_used_mb": 0,
"data_remaining_mb": 1024,
"expires_at": "2027-05-05 16:10:49",
"source": "live",
"checked_at": "2026-11-06T16:13:31+00:00"
}
GET /esims/{iccid}/topups and POST /esims/{iccid}/topup
List the top-up packs the network offers for this eSIM, priced for you, then buy one with {"topup_id":"T12","partner_ref":"your-ref"}. Day passes take D3 for three more days. An empty list means the plan has no top-up; sell a new eSIM instead.
{
"iccid": "8910300000059801256",
"topups": []
}
POST /esims/{iccid}/cancel
Cancel an eSIM that was never installed. The price goes back to your wallet. An installed eSIM cannot be cancelled (409 not_cancellable).
{
"cancelled": true,
"iccid": "8988931820342767218",
"refunded": 0,
"mode": "test",
"note": "SANDBOX eSIM. Nothing was sent to the network and no wallet was touched."
}
POST /esims/{iccid}/revoke, /suspend, /unsuspend
Revoke kills an eSIM in any state with no refund. Suspend pauses data, unsuspend resumes it.
POST /esims/{iccid}/refund-request
Body {"reason":"never worked at destination"}. An eSIM that was never installed is cancelled and refunded on the spot ("mode":"auto"). Anything else is queued for a manual review ("mode":"queued") and you get an email with the decision. Plans with a phone number are never refundable.
GET /wallet
Balance, the low balance alert level and your last transactions (?limit= to page). Top up from the portal: Wallet. Test keys never change the balance.
{
"balance": 3.2,
"currency": "USD",
"low_balance_threshold": 25,
"mode_note": null,
"transactions": [
{
"id": 3,
"type": "debit",
"amount": -1.8,
"balance_after": 3.2,
"method": "order",
"reference": null,
"order_number": "ORD-C202EA1F",
"note": "Order ORD-C202EA1F (P1465 x1)",
"created_at": "2026-11-06 16:10:47"
},
{
"id": 1,
"type": "topup",
"amount": 5,
"balance_after": 5,
"method": "manual",
"reference": "manual-test-credit-1",
"order_number": null,
"note": "Test credit for the live order check",
"created_at": "2026-11-06 16:06:44"
}
]
}
Webhooks
We POST a JSON body to your URL when something happens to a live eSIM or your wallet. Register up to 10 https URLs; private and local hosts are refused. Sandbox orders never send webhooks.
Events
esim.ready, esim.installed, esim.active, esim.low_data, esim.expiring, esim.used_up, esim.expired, esim.suspended, esim.cancelled, esim.revoked, order.failed, wallet.low_balance, wallet.topped_up. Register with "events": ["*"] to get them all.
Payload
{
"event": "esim.ready",
"created": "2026-11-06T16:13:00+00:00",
"data": { ...the same object GET /orders/{id} returns... }
}
esim.ready carries the full order. esim.cancelled, esim.revoked, esim.suspended, esim.active carry iccid, order_number, partner_ref (and refunded for cancel/revoke). order.failed carries order_number and partner_ref. wallet.low_balance carries balance and threshold. wallet.topped_up carries amount, balance, reference. A test delivery uses the event name ping.
Headers and signature
Every delivery has X-AfriEsim-Event: <event> and X-AfriEsim-Signature: sha256=<hex>, where hex is HMAC-SHA256 of the raw request body with your webhook secret (shown in GET /account and in the portal). Compute it over the exact bytes you received and compare before trusting the payload. Answer with any 2xx within 4 seconds; do the work afterwards.
Retries
A non-2xx answer or a timeout is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then dropped. Five failures in a row pause the URL for a minute; after five pauses it is disabled until you send a test from the portal or the API. Deliveries go out right after the event, during our next request handling; a quiet account may see a delay of a few minutes.
POST /webhooks
Register a URL
curl -X POST -H "Authorization: Bearer af_test_YOUR_KEY" -H "Content-Type: application/json" \\
-d '{"url":"https://afriesim.net/api/v1/partner/_echo.php","events":["esim.ready","order.failed","wallet.topped_up"]}' https://afriesim.net/api/v1/partner/webhooksResponse (201):
{
"id": 1,
"url": "https://afriesim.net/api/v1/partner/_echo.php",
"events": [
"esim.ready",
"order.failed",
"wallet.topped_up"
],
"active": true,
"disabled_at": null,
"blocked_until": null,
"consecutive_failures": 0,
"created_at": "2026-11-06 16:04:47",
"secret": "02b3abb3...(48 hex chars)",
"signature_header": "X-AfriEsim-Signature: sha256=HMAC_SHA256(body, secret)"
}
https://afriesim.net/api/v1/partner/_echo.php is a test target that answers 200 to anything; use it to see a delivery succeed before you have your own endpoint.
GET /webhooks
{
"events": [
"esim.ready",
"esim.installed",
"esim.active",
"esim.low_data",
"esim.expiring",
"esim.used_up",
"esim.expired",
"esim.suspended",
"esim.cancelled",
"esim.revoked",
"order.failed",
"wallet.low_balance",
"wallet.topped_up"
],
"webhooks": [
{
"id": 1,
"url": "https://afriesim.net/api/v1/partner/_echo.php",
"events": [
"esim.ready",
"order.failed",
"wallet.topped_up"
],
"active": true,
"disabled_at": null,
"blocked_until": null,
"consecutive_failures": 0,
"created_at": "2026-11-06 16:04:47"
}
]
}
POST /webhooks/{id}/test
Sends a ping event right now and re-enables a disabled URL.
{
"sent": true,
"delivery": {
"id": 1,
"event": "ping",
"attempts": 1,
"response_code": 200,
"delivered_at": "2026-11-06 16:05:58",
"next_retry_at": null,
"created_at": "2026-11-06 16:05:42"
}
}
GET /webhooks/{id}/deliveries
{
"webhook": {
"id": 1,
"url": "https://afriesim.net/api/v1/partner/_echo.php",
"events": [
"esim.ready",
"order.failed",
"wallet.topped_up"
],
"active": true,
"disabled_at": null,
"blocked_until": null,
"consecutive_failures": 0,
"created_at": "2026-11-06 16:04:47"
},
"deliveries": [
{
"id": 4,
"event": "esim.ready",
"attempts": 1,
"response_code": 200,
"delivered_at": "2026-11-06 16:13:16",
"next_retry_at": null,
"created_at": "2026-11-06 16:13:00"
},
{
"id": 2,
"event": "wallet.topped_up",
"attempts": 1,
"response_code": 200,
"delivered_at": "2026-11-06 16:09:36",
"next_retry_at": null,
"created_at": "2026-11-06 16:06:44"
}
]
}
DELETE /webhooks/{id}
Removes the URL and its delivery history. Returns {"deleted": true, "id": 1}.
A typical integration
- Create a test key in the portal. Call
GET /catalogue/plans?country=XXfor the destinations you sell and cache the result for an hour. - When your customer pays you, call
POST /orderswith your own order id aspartner_ref. Store the returnedorder_numberand the eSIM'siccid,lpaandqr_url. - If the eSIM comes back with status
processing, pollGET /orders/{id}every 30 seconds for up to 10 minutes, or register a webhook foresim.ready. - Show your customer the QR image (or the LPA string) and the install steps. Call
GET /esims/{iccid}/usagewhen they ask how much data is left. - If something goes wrong on your side before the customer installs, call
POST /esims/{iccid}/cancelto get the money back to your wallet. - Once approved, top up the wallet, create a live key, and switch the key in your config. Nothing else changes between sandbox and live.
Questions or a feature you need? Write to support@afriesim.net. Reseller terms: /pages/reseller-terms/.
