Aerix SIM API: Integration Guide
Order, track and provision Aerix physical SIMs and eSIMs from your own systems.
What the API does
The Aerix SIM API gives your systems the same view of your SIM estate that our own team has. With it you can (i) list every SIM and eSIM we have issued to you and see where each one is in its lifecycle, (ii) fetch the credentials you need to provision a physical SIM on a core you run yourself, (iii) order new programmed SIMs against your credit account, and (iv) be told by webhook when an order is ready or a SIM changes state, rather than polling for it.
It is a JSON over HTTPS API at https://api.aerix.uk/v1, described by an OpenAPI 3.1 document at https://api.aerix.uk/v1/openapi.json. That document is the contract; this page explains how the pieces fit together. Most tooling (Postman, Insomnia, openapi-generator, Swagger UI) can import it directly to give you a typed client.
Getting access
Every request carries an API key belonging to your Aerix account. We will issue your first key and send it as a one-time 1Password link, never in an email. If you use the Aerix customer portal you can also create, scope and revoke keys yourself under Account → API keys. A key is shown once, when it is created; we keep only a hash of it, so store it in your secrets manager straight away.
Each key carries one or more scopes, so you can give a system only what it needs:
| Scope | Allows |
|---|---|
sims:read | List and read SIMs, orders, profiles, events and your account |
credentials:read | Read K, OPc and the SUCI public key for your physical SIMs. Every call is logged. |
orders:write | Place SIM orders on your credit account |
sims:write | Label, suspend, resume and retire SIMs |
webhooks:write | Manage webhook endpoints |
esims:write | Reserved for eSIM issuing, which opens once our SM-DP+ is live |
Ordering also needs a credit account, which we set up with you: orders are invoiced on your payment terms rather than paid by card. Ask us at support@aerix.uk if you want to order through the API.
Quick start
Check the key works and see what it can do:
export AERIX_KEY=aerix_live_...
curl -s https://api.aerix.uk/v1/account -H "Authorization: Bearer $AERIX_KEY"{
"customer_id": "0b7e4d2a-...",
"name": "Your Company Ltd",
"credit": { "limit_pence": 200000, "currency": "gbp", "payment_terms_days": 30,
"on_hold": false, "exposure_pence": 0, "headroom_pence": 200000, "overdue": false },
"key": { "id": "88e3...", "scopes": ["sims:read", "credentials:read"] }
}List your SIMs, newest first:
curl -s "https://api.aerix.uk/v1/sims?limit=50" -H "Authorization: Bearer $AERIX_KEY"{
"data": [
{
"iccid": "89440900100000012345",
"imsi": "999700000000001",
"form": "physical",
"state": "ready",
"site": null,
"label": null,
"created_at": "2026-08-28T18:02:11Z",
"physical": { "batch_id": "6b1c...", "order_id": null, "msisdn": null, "profile": "99970" },
"esim": null
}
],
"next_cursor": null
}Filter with state, form, site, profile, iccid or imsi, and fetch one SIM with GET /sims/{iccid}.
SIMs and their lifecycle
Physical SIMs and eSIMs share one model. form says which it is, and the physical or esim object carries the detail that only applies to that form. Every SIM has a state:
| State | Physical SIM | eSIM |
|---|---|---|
provisioning | Queued or being programmed on our SIM machine | Download order in progress at the SM-DP+ |
ready | Programmed; credentials available. On your own core, this is its normal state. | Released; activation code available |
active | A live subscriber on an Aerix-run core | Installed on a device |
suspended | Barred on an Aerix-run core | Barred on an Aerix-run core |
retired | Finished with; its identity will not be reused | Profile deleted or retired at the SM-DP+ |
failed | Programming failed | SM-DP+ refused the profile |
unknown | We cannot tell yet | Never synced with the SM-DP+ |
site names the Aerix-run network whose core serves the SIM. It is null for SIMs that live on a core you run, which is why a SIM on your own core stays ready rather than becoming active: we cannot see your core, so we do not guess. New states may be added over time; treat a state you do not recognise as you would unknown.
Credentials for your own core
If you run your own 5G core, you need each physical SIM's K and OPc to provision it as a subscriber, and the SUCI home-network public key that we wrote to your cards (the matching private key is yours, from the customer portal). With the credentials:read scope:
curl -s https://api.aerix.uk/v1/sims/89440900100000012345/credentials -H "Authorization: Bearer $AERIX_KEY"{
"iccid": "89440900100000012345",
"imsi": "999700000000001",
"k": "3F6A...32 hex characters...",
"opc": "9B10...32 hex characters...",
"suci": { "scheme": "profile_a", "key_id": 1, "public_key_hex": "5A8D...A650" }
}Treat these as secrets: anyone with K and OPc can clone the subscriber. Fetch them from the system that provisions your core, write them straight into it, and avoid logging the response. We log every credentials call against the key that made it. eSIM keys never leave the SM-DP+, so this returns 409 not_physical for an eSIM.
Ordering SIMs on account
With a credit account and the orders:write scope you can order between 1 and 50 programmed SIMs per request. You choose the network name shown on handsets (SPN), your PLMN (MCC and a 2-digit MNC) and the first IMSI. IMSIs then run sequentially. Every POST needs an Idempotency-Key: if your request times out, send it again with the same key and you will get the original response back rather than a second order.
curl -s -X POST https://api.aerix.uk/v1/orders \
-H "Authorization: Bearer $AERIX_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: po-4471-batch-1" \
-d '{
"form": "physical",
"quantity": 20,
"spn": "Example Networks",
"mcc": "999",
"mnc": "70",
"start_imsi": "999700000000101",
"suci_calculation": "sim",
"reference": "PO-4471",
"shipping": {
"name": "Lab team", "line1": "1 Example Street", "city": "Bath",
"postcode": "BA1 1AA", "country": "GB", "phone": "+44 1225 000000"
}
}'A successful order returns 201 with the order, whose status then moves through accepted → provisioning → ready → dispatched. We raise an invoice for it straight away on your payment terms, and the order's invoice object tracks it. The order is refused, and nothing is programmed or charged, if:
| Response | Why |
|---|---|
402 credit_limit_exceeded | It would take you over your credit limit. The body gives your headroom. |
403 no_credit_account | Your account has no credit facility yet |
403 account_on_hold | An invoice is overdue; ordering resumes once it is paid |
400 invalid_request | The SIM specification or address is invalid; detail says what |
422 idempotency_conflict | You reused an Idempotency-Key for a different order |
Once the order is ready, its SIMs appear in GET /sims?profile=… with credentials available. eSIM ordering through the API ("form": "esim") returns 409 esim_orders_unavailable until our SM-DP+ goes live; talk to us about eSIMs meanwhile.
Suspend, resume and retire
With sims:write, POST /sims/{iccid}/suspend bars a SIM and /resume restores it, on networks where we run the core (where site is set). For SIMs on your own core these return 409 core_not_managed: we have no access to your core, so bar the subscriber there. POST /sims/{iccid}/retire works everywhere: it bars the SIM on any Aerix core, retires an eSIM at the SM-DP+, and records the SIM as retired so its identity is never reissued. It is final. Like all POSTs these need an Idempotency-Key. PATCH /sims/{iccid} with {"label": "…"} sets your own label for a SIM.
Events and webhooks
Changes are recorded as events, each with an increasing seq. The types today are order.created, order.updated, sim.state_changed, sim.updated; more may be added, so ignore types you do not handle. You can poll for them:
curl -s "https://api.aerix.uk/v1/events?after=0" -H "Authorization: Bearer $AERIX_KEY"
# then pass the returned next_after as ?after= next timeOr register an HTTPS endpoint with the webhooks:write scope and we will POST each event to it. The response includes a signing secret, shown only once:
curl -s -X POST https://api.aerix.uk/v1/webhooks \
-H "Authorization: Bearer $AERIX_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: webhook-prod-1" \
-d '{"url": "https://provisioning.example.com/aerix/events"}'Each delivery carries X-Aerix-Event and X-Aerix-Signature: t=<unix seconds>,v1=<hex>, where the hex is an HMAC-SHA256 of <t>.<raw request body> keyed with your secret. Check it before trusting the body, reject timestamps more than five minutes old, and reply with any 2xx within 10 seconds. Anything else is retried with backoff for 24 hours, and the same events stay available from /events, so you can catch up after an outage. Deliveries can arrive more than once; use the event id to ignore repeats.
Verifying a signature (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyAerix(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const t = Number(parts.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === parts.v1?.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}Verifying a signature (Python)
import hmac, hashlib, time
def verify_aerix(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Errors
Errors are RFC 9457 application/problem+json. Branch on the stable code field; title and detail are for people and may be reworded.
{
"type": "https://api.aerix.uk/problems/core_not_managed",
"title": "This SIM is not on an Aerix-run core",
"status": 409,
"code": "core_not_managed",
"detail": "Bar or restore the subscriber on your own core; Aerix has no access to it."
}| Status | Codes |
|---|---|
| 400 | invalid_request, idempotency_key_required |
| 401 | unauthorized |
| 402 | credit_limit_exceeded |
| 403 | insufficient_scope, no_credit_account, account_on_hold |
| 404 | not_found (also for anything that is not yours) |
| 409 | invalid_transition, core_not_managed, not_physical, not_supported, esim_orders_unavailable, idempotency_in_progress |
| 422 | idempotency_conflict |
| 429 | rate_limited, with Retry-After |
| 5xx | internal_error, core_unavailable: nothing changed, retry with the same Idempotency-Key |
Limits, paging and compatibility
Each key may make 600 read and 60 write requests a minute; beyond that you get 429 with a Retry-After header. Lists return {"data": [...], "next_cursor": ...}; pass next_cursor back as ?cursor= until it is null, with limit from 1 to 500 (default 100). ICCIDs and IMSIs are always strings, never numbers, and timestamps are RFC 3339 in UTC. Every response carries Aerix-Api-Version.
We make one promise about change: v1 only grows. We add fields, endpoints, query parameters, event types, states and error codes, and we never remove, rename or retype anything you already rely on. Ignore what you do not recognise and your integration will keep working. If we ever need an incompatible change it will ship as /v2, with v1 left running.
A suggested integration
For a team running its own core and provisioning system, a typical integration looks like this:
- Register a webhook for
order.updatedandsim.state_changed, and store the secret. - Place an order with an Idempotency-Key derived from your own purchase order, so a retry can never double-order.
- When an
order.updatedevent says the order isready, list its SIMs withGET /sims?profile=…. - For each SIM, fetch
/credentialsand create the subscriber on your core (IMSI, K, OPc; install the SUCI private key once per profile). - Label each SIM with your own asset or device reference using
PATCH /sims/{iccid}. - Run a nightly reconciliation with
GET /simsandGET /events?after=…, so nothing is missed if a webhook was down. - When hardware is decommissioned, remove the subscriber from your core and call
/retire.
Questions, a test key, or something the API does not yet do: email support@aerix.uk. We would rather hear about a missing field early than have you work around it.
