Skip to content

Nesoraa API

Provision connectivity from your own product

A REST API over the eSIM lifecycle: browse the catalogue, provision a profile, follow the operation to its outcome, and read usage back.

Your first request

Every endpoint takes a bearer token and returns JSON. Listing plans needs no setup beyond a key, which makes it the cheapest way to confirm one works.

curl https://api.nesoraa.com/v1/v1/plans \
  -H "Authorization: Bearer nsk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
A 200 with an empty array means the key is valid and the catalogue is not open to it yet.

Authentication

Send the key as a bearer token. Keys are shown once at creation and stored hashed — we cannot recover one for you, only replace it.

curl https://api.nesoraa.com/v1/v1/esims \
  -H "Authorization: Bearer nsk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# No credential, or one that does not parse:
# 401 { "error": { "type": "unauthorized" } }
#
# A valid key whose scopes do not cover the route:
# 403 { "error": { "type": "forbidden" } }

Environments

There are exactly two, and both exist for every project from the moment it is created.

EnvironmentKey prefixWhat it does
Livensk_live_…Real money moves, real profiles are provisioned, real customers are affected.
Testnsk_test_…Nothing leaves the building. Same endpoints, same shapes, no provider call.

The environment is the key, not the URL.

Both environments are served from the same origin — https://api.nesoraa.com/v1 — and the credential decides which data you reach. There is no separate sandbox hostname to switch to, so moving to production means swapping the key and nothing else. A test key cannot touch live data even if a query is written wrong.

Asynchronous provisioning

Provisioning reaches a mobile network, so it cannot complete inside your request. POST /v1/esims reserves inventory, debits the wallet and records the intent in one transaction, then returns 202.

202 means accepted, not provisioned.

At that point you have an eSIM record and an operation. You do not have a profile, and activation credentials do not exist yet. Poll the operation, or wait for the esim.provisioned event on a webhook endpoint.

# 1. Request provisioning. 202 means ACCEPTED, not PROVISIONED.
#    Idempotency-Key is required: retrying with the same key returns
#    the same operation instead of provisioning twice.
curl -X POST https://api.nesoraa.com/v1/v1/esims \
  -H "Authorization: Bearer nsk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"plan_id": "plan_4KQ2m8"}'

# 2. The 202 body carries the eSIM and its operation. Poll the
#    operation until it leaves "pending".
curl https://api.nesoraa.com/v1/v1/operations/op_7FK2c9 \
  -H "Authorization: Bearer nsk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# 3. Once the operation succeeded, retrieve the eSIM. Activation
#    credentials appear HERE and never in a list response.
curl https://api.nesoraa.com/v1/v1/esims/esim_3QN8f2 \
  -H "Authorization: Bearer nsk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Retrying with the same Idempotency-Key returns the original operation rather than provisioning twice.

Reading the outcome

  • succeeded — the profile exists. Retrieve the eSIM for its activation credentials.
  • failed — nothing was provisioned and the inventory claim was released. Safe to try again.
  • uncertain — the request reached the network and the outcome is unknown. Do not retry. Reconciliation settles it; retrying risks a second profile against one payment.

Nesoraa provisions eSIMs through a provider-independent API.

Authentication

Every request carries Authorization: Bearer <api key>. A key belongs to exactly one project and one environment, which is why no request takes a project or environment parameter — the credential decides both.

Environments

A key is test or live. They are separate worlds: a test key cannot see live eSIMs, cannot reserve live inventory, and cannot resolve a plan that is only mapped for live.

Idempotency

Every request that spends money requires an Idempotency-Key header. Retrying with the same key returns the original result forever — it never buys a second eSIM. This is durable, stored alongside the purchase rather than cached, so a restart on either side cannot lose the protection.

Asynchrony

POST /v1/esims returns 202, not 201. Nesoraa has taken payment and reserved an eSIM; the upstream network has not been contacted yet. Poll the eSIM or its operation, or wait for a webhook. An eSIM is installable when its activation object appears.

Rate limits

Limits protect the platform, not your plan — they are generous relative to any normal integration and a correct client should never see one.

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can pace a client without ever hitting a 429. When you do, Retry-After is a whole number of seconds and is never zero; the request was not performed, so retrying after that interval is safe.

Budgets are per API key, and also per project and per organization, so one noisy key cannot exhaust a colleague's. test and live hold separate budgets. Requests that spend money or reach a mobile network — creating an eSIM, sending a message — have a much smaller budget than reads.

Errors

Every failure carries a stable type to switch on, a specific code, a human message, and the request_id to quote to support.

Webhooks

Register an endpoint and Nesoraa POSTs every event it produces to it. The body is the same event object GET /v1/events returns, so a receiver and a poller parse the same shape.

Verifying a delivery

Every delivery carries a Nesoraa-Signature header:

HTTP
Nesoraa-Signature: t=1785499331,v1=6f3a…

t is the Unix timestamp in seconds. Each v1 is HMAC-SHA256 over the exact string `${t}.${rawBody}`, keyed with your endpoint's signing secret, hex encoded.

Delivery and retries

Delivery is at-least-once: a receiver must be idempotent on event.id. A non-2xx response or a timeout is retried with exponential backoff for a bounded number of attempts; a delivery that exhausts them is kept and can be replayed from the portal or through POST /v1/webhook_deliveries/{id}/replay. Repeated failure disables the endpoint rather than retrying forever, and the reason is readable on the endpoint itself.

The secret

It is shown once, when the endpoint is created or its secret rotated, and is stored encrypted — it cannot be read back through the API or the portal, only replaced.

API reference

Every endpoint, its required scopes, parameters and responses — generated from the schema this service publishes.

Standard errors

Every endpoint below can return these. Each operation lists only the statuses that are specific to it.

StatusMeaning
400Invalid request.
401No valid credential was presented.
403Valid credential, insufficient scope.
429Rate limited. Wait for the number of seconds in `Retry-After`, then retry — the request was not performed, so retrying is safe. `RateLimit-Remaining` and `RateLimit-Reset` are also present on successful responses, so a well-behaved client never needs to reach this status to discover its budget.
500Something broke on our side. Retryable.

Plans

The catalogue.

List countries and whether they are purchasable

GET/v1/countriescatalog:read

Where Nesoraa can carry traffic. ⚠️ `availability` is derived from the plan catalogue, not from network coverage: a country is only `available` when an active plan covers it. `coming_soon` means we can reach it but no plan exists yet, and `unsupported` means we cannot serve it. Do not treat a listed country as buyable — check `availability`, then find a plan in /v1/plans.

Response

StatusMeaning
200A page of countries.

List purchasable plans

GET/v1/planscatalog:read

The catalogue as this project may buy it. Prices are what you pay per eSIM. A plan reads the same in both environments; what differs is whether it can be provisioned, which surfaces at purchase.

Response

StatusMeaning
200A page of plans.

eSIMs

Provisioned connectivity.

List eSIMs

GET/v1/esimsesims:read

⚠️ Activation credentials are never included in a list. Retrieve a single eSIM to obtain them.

Parameters

NameTypeInRequiredDescription
limitintegerqueryOptionalPage size. Values above the maximum are clamped rather than rejected, so asking for everything returns a page and a cursor instead of a 400.
cursorstringqueryOptionalThe `next_cursor` from a previous page. Omit for the first page. A page whose `next_cursor` is null is the last one.
statepending | active | suspended | disconnected | failedqueryOptional

Response

StatusMeaning
200A page of eSIMs.

Buy an eSIM

POST/v1/esimsesims:write

Reserves inventory, debits the wallet and records the intent to provision — in one database transaction — then returns **202**. The upstream network has NOT been contacted when this returns. Poll the eSIM, poll its operation, or wait for `esim.provisioned`. ⚠️ `Idempotency-Key` is required. Retrying with the same key returns the same eSIM; a different key buys a second one.

Parameters

NameTypeInRequiredDescription
Idempotency-KeystringheaderRequiredUnique per purchase, reused on retry. A UUID is a good choice.

Request body

Required, as application/json. The full schema is in the OpenAPI document.

Response

StatusMeaning
202Accepted. Provisioning has not happened yet.

Errors

In addition to the standard statuses

StatusMeaning
402Wallet balance too low. Nothing was charged.
409No inventory, or the plan cannot be provisioned here. Nothing was charged.

Retrieve an eSIM, with activation credentials

GET/v1/esims/{id}esims:read

The only endpoint that returns activation credentials. `activation` appears once the eSIM is `active` or `suspended`. It is a bearer credential: whoever holds `lpa` can install the profile. Show it to your end user and do not log it.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.

Response

StatusMeaning
200The eSIM.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM under this credential.

Retrieve consumption and serving network

GET/v1/esims/{id}/usageesims:read

How much of the allowance is left, and which network is carrying it. ⚠️ **These figures are polled, not live.** `observed_at` says when we last read them and `stale` says whether we still vouch for the reading. There is no upstream push and no bulk endpoint, so usage is synchronised in the background — an endpoint that called the network on every request would let one integration exhaust the shared budget. Every field is nullable, and that is the honest shape: an eSIM that has never been used has no counters, no activation time and no network. Zero would be a different and false statement. `period.activated_at` is when the allowance clock STARTED — first use, not purchase.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.

Response

StatusMeaning
200The current snapshot.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM under this credential.

Suspend this eSIM

POST/v1/esims/{id}/suspendesims:write

Stop this eSIM carrying traffic, reversibly. ⚠️ **Accepted, not applied.** The response is `202`: the operation is queued and a worker asks the network moments later. Nothing about the eSIM has changed when this returns — poll the operation for the outcome. Only an `active` eSIM can be suspended. Anything else is a `409` naming the current state, so your code can branch without polling. An `Idempotency-Key` header is required. Retrying with the same key returns the same operation rather than creating a second one.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.
Idempotency-KeystringheaderRequiredUnique per action, reused on retry. A UUID is a good choice.

Response

StatusMeaning
202The operation was recorded and is queued. Poll `GET /v1/operations/{id}` for the outcome.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM under this credential.
409This eSIM is not active, or another operation is already in flight.

Resume a suspended eSIM

POST/v1/esims/{id}/resumeesims:write

Return a suspended eSIM to service. ⚠️ **Accepted, not applied** — see suspend. The `202` means queued. Only a `suspended` eSIM can be resumed. A disconnected one cannot: that state is final in Nesoraa and a replacement is a new purchase. An `Idempotency-Key` header is required.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.
Idempotency-KeystringheaderRequiredUnique per action, reused on retry. A UUID is a good choice.

Response

StatusMeaning
202The operation was recorded and is queued. Poll `GET /v1/operations/{id}` for the outcome.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM under this credential.
409This eSIM is not suspended, or another operation is already in flight.

Disconnect this eSIM permanently

POST/v1/esims/{id}/disconnectesims:write

⚠️ **Final.** End this eSIM’s service permanently. A disconnected eSIM is never returned to service through this API. The upstream subscriber record survives — this is not the supplier’s irreversible end-of-life state, which Nesoraa cannot reach at all — but Nesoraa treats the eSIM as finished, because re-selling a cancelled subscription would silently restore service a customer ended. Getting service again means buying a new eSIM. ⚠️ **Accepted, not applied.** The `202` means queued; poll the operation. An `Idempotency-Key` header is required.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.
Idempotency-KeystringheaderRequiredUnique per action, reused on retry. A UUID is a good choice.

Response

StatusMeaning
202The operation was recorded and is queued. Poll `GET /v1/operations/{id}` for the outcome.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM under this credential.
409This eSIM is already disconnected, or another operation is already in flight.

Retrieve the last known approximate network position

GET/v1/esims/{id}/locationlocation:read

Where the mobile network last placed this eSIM. ⚠️ **This is an approximate position derived from the serving cell** — not GPS, not device-reported, and not consented to by the handset. `position.accuracy_meters` is a median error radius: the eSIM is somewhere within roughly that distance, not at the point given. Do not build a geofence on it without accounting for that radius. **It requires its own scope, `location:read`, and `esims:read` will not work.** Position says where a person is, so a credential issued to read eSIM state does not acquire the ability to track handsets. Grant it deliberately. **Positions are pushed by the network, not polled by us**, on a cadence the network decides. That is why the response carries `age_seconds` rather than a `stale` flag as usage does — we cannot honestly tell you a threshold for data whose arrival we do not control, so you apply the one your use case justifies. A **404 with code `location_not_available`** is a normal answer, and a common one: an eSIM that has never attached to a network, or whose network does not report position, will not have one. It is distinct from `esim_not_found`.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.

Response

StatusMeaning
200The last known position.

Errors

In addition to the standard statuses

StatusMeaning
404No such eSIM (`esim_not_found`), or no position has ever been reported for it (`location_not_available`).

Operations

Asynchronous work, and how it ended.

List operations

GET/v1/operationsesims:read

Parameters

NameTypeInRequiredDescription
limitintegerqueryOptionalPage size. Values above the maximum are clamped rather than rejected, so asking for everything returns a page and a cursor instead of a 400.
cursorstringqueryOptionalThe `next_cursor` from a previous page. Omit for the first page. A page whose `next_cursor` is null is the last one.
statepending | executing | uncertain | succeeded | failedqueryOptional
esim_idstringqueryOptional

Response

StatusMeaning
200A page of operations.

Retrieve an operation

GET/v1/operations/{id}esims:read

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe operation id.

Response

StatusMeaning
200The operation.

Errors

In addition to the standard statuses

StatusMeaning
404No such operation under this credential.

Wallet

Balance and the immutable ledger.

Retrieve the balance

GET/v1/walletwallet:read

One balance per billing account, shared by both environments — there is no separate test balance. Read-only: adding funds is a payment, not an API call.

Response

StatusMeaning
200The wallet.

Errors

In addition to the standard statuses

StatusMeaning
409This project has no billing account.

List ledger entries

GET/v1/wallet/transactionswallet:read

Append-only, newest first. `reference` is the resource an entry paid for, so a line on a statement joins back to the eSIM it bought.

Parameters

NameTypeInRequiredDescription
limitintegerqueryOptionalPage size. Values above the maximum are clamped rather than rejected, so asking for everything returns a page and a cursor instead of a 400.
cursorstringqueryOptionalThe `next_cursor` from a previous page. Omit for the first page. A page whose `next_cursor` is null is the last one.
typeCREDIT | DEBIT | REFUNDqueryOptional

Response

StatusMeaning
200A page of ledger entries.

Messages

SMS to a provisioned eSIM.

Send an SMS to this eSIM

POST/v1/esims/{id}/messagesmessages:write

Returns **202**. The message is recorded and authorised; the mobile network has not been contacted yet. Poll the message or wait for a webhook. ⚠️ **There is no delivery receipt.** The strongest state a message can reach is `accepted` — the network took it for onward transmission. No part of this API can tell you a handset displayed anything, and `accepted` must not be read as delivery. ⚠️ **You cannot choose an arbitrary sender.** `sender` may name only an identity registered to your project AND approved by an operator, and only if your project permits per-request senders. Otherwise the project default is used. This prevents impersonation and is not configurable by the caller. `text` is limited to 140 printable ASCII characters. Non-Latin scripts and emoji are refused rather than transmitted incorrectly. `Idempotency-Key` is required: an SMS cannot be unsent.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe eSIM id.
Idempotency-KeystringheaderRequiredUnique per message, reused on retry.

Request body

Required, as application/json. The full schema is in the OpenAPI document.

Response

StatusMeaning
200Idempotent replay of an earlier message.
202Accepted for dispatch. Not yet sent.

Errors

In addition to the standard statuses

StatusMeaning
409Sender not permitted, eSIM unreachable, or idempotency key reused.

Events

Everything that has happened.

List events

GET/v1/eventsevents:read

Parameters

NameTypeInRequiredDescription
limitintegerqueryOptionalPage size. Values above the maximum are clamped rather than rejected, so asking for everything returns a page and a cursor instead of a 400.
cursorstringqueryOptionalThe `next_cursor` from a previous page. Omit for the first page. A page whose `next_cursor` is null is the last one.
typestringqueryOptional
resource_idstringqueryOptional
actor_idstringqueryOptional

Response

StatusMeaning
200A page of events.

Retrieve an event

GET/v1/events/{id}events:read

Use this to verify a webhook you did not authenticate: if the id in a delivery does not exist under your own credential, the delivery was not ours.

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe event id.

Response

StatusMeaning
200The event.

Errors

In addition to the standard statuses

StatusMeaning
404No such event under this credential.

Webhooks

Delivery of events to your endpoint.

List webhook endpoints

GET/v1/webhook_endpointswebhooks:read

Response

StatusMeaning
200Success.

Create a webhook endpoint

POST/v1/webhook_endpointswebhooks:write

Response

StatusMeaning
200Success.

Retrieve a webhook endpoint

GET/v1/webhook_endpoints/{id}webhooks:read

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

Update a webhook endpoint

PATCH/v1/webhook_endpoints/{id}webhooks:write

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

Delete a webhook endpoint

DELETE/v1/webhook_endpoints/{id}webhooks:write

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

Rotate the signing secret

POST/v1/webhook_endpoints/{id}/rotate_secretwebhooks:write

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

List delivery attempts

GET/v1/webhook_deliverieswebhooks:read

Response

StatusMeaning
200Success.

Retrieve a delivery attempt

GET/v1/webhook_deliveries/{id}webhooks:read

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

Replay a delivery

POST/v1/webhook_deliveries/{id}/replaywebhooks:write

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.

API keys

Credential lifecycle.

List API keys

GET/v1/keyskeys:read

Response

StatusMeaning
200Success.

Create an API key — the secret is shown once

POST/v1/keyskeys:write

Response

StatusMeaning
200Success.

Revoke an API key

DELETE/v1/keys/{id}keys:write

Parameters

NameTypeInRequiredDescription
idstringpathRequiredThe resource id.

Response

StatusMeaning
200Success.