API documentation

Build wallet balance, eSIMs and airtime & data into your own product. JSON everywhere, Bearer-token auth, prices in NGN, and an automatic refund to your wallet if an order can't be completed.

Getting started

Four steps from zero to a working call:

  1. Create an account.
  2. Fund your wallet from the Wallet page. Every purchase spends this one shared balance, so an empty wallet means every purchase endpoint returns 402.
  3. Generate a key in the Developer console. Keys look like odx_live_… and are shown once — copy it somewhere safe before closing the dialog.
  4. Call the base URL below. Every response is JSON.
Base URL
https://your-onedex-domain/api/v1

# Verify your key works — costs nothing
curl https://your-onedex-domain/api/v1/balance \
  -H "Authorization: Bearer odx_live_..."

There is no separate sandbox. Read endpoints (/balance, /esim/packages, /bills/data-plans) are free to call and are the safe way to test your integration end to end. Anything that spends money is live from the first request, so test with the smallest amount a service allows — ₦50 of airtime, for instance.

What you can connect

Reading and buying are separate questions here. Every catalog is readable over the API, priced for your account, so you can build a storefront against all of Onedex. Buying is programmable wherever one request can return a finished product — eSIMs, airtime and data.

The rest keep checkout in the dashboard because the purchase isn't finished when the response would be: a number is dead until an SMS arrives, and a Checkmark request is fulfilled by hand over days. Those need a status endpoint to poll, not just a POST.

ServiceAPIHow
Wallet YesGET /balance — read the spendable balance.
eSIM YesGET /esim/packages, POST /esim/orders — full purchase flow.
Airtime & Data YesGET /bills/data-plans, POST /bills/airtime, POST /bills/data.
Marketplace YesGET /store/products, POST /store/orders — the response carries the purchased items.
Numbers (verification) YesGET /verify/countries, /verify/services, /verify/servers, POST /verify/activations then poll and cancel — plus /verify/rentals for long-term lines. A number is useless until an SMS lands, so buying one is a short sequence of calls rather than a single request.
Checkmark Read onlyGET /checkmark/listings — catalog and prices. Ordering is dashboard-only: requests are fulfilled by hand over days, and credential-type listings take an account password.
Subscriptions YesGET /topup/listings, POST /topup/orders then poll /topup/orders/{id}. Requests are fulfilled by hand, so the price is held in escrow and the poll is minutes apart — completed charges it, failed hands it back in full.
Boost YesGET /boost/servers, /boost/services, POST /boost/orders then poll, cancel or refill. Charged upfront and delivered over time, so ordering is a place-then-poll sequence — the poll is also what settles the refund for undelivered units.
Wallet funding NoDashboard only — top up via Korapay, bank transfer or crypto.

All of it draws on the same wallet, so a balance you top up in the dashboard is immediately spendable by the API, and vice versa.

Authentication

Pass your key as a Bearer token on every request, including the catalog reads. There are no unauthenticated endpoints.

Authorization: Bearer odx_live_your_key_here

A key inherits its owner's wallet, Premium tier and discount. Anyone holding it can spend that wallet, so treat it like a password: keep it server-side, never ship it in a browser bundle or mobile app, and never commit it. If a key leaks, revoke it in the Developer console — revocation takes effect on the next request and cannot be undone.

Missing, malformed, unknown and revoked keys all return the same 401, deliberately — the API will not tell a caller whether a key once existed.

How billing works

Every purchase endpoint follows the same sequence:

  1. The price is calculated for your account, including any discount.
  2. That amount is debited from your wallet. If the balance is short, you get 402 and nothing else happens.
  3. The order is placed with the upstream provider.
  4. On success the order is recorded and a ledger entry written. On failure the debit is reversed in full and you get 502.

Because the debit happens first, a failed purchase never leaves you charged — but it also means the wallet dips briefly during the call. Sequential purchases from a thin balance can fail on the second call even though the first was refunded milliseconds later.

One-time numbers are the exception. Buying a number holds its price instead of spending it. The hold leaves your balance immediately — so /balance drops — but it is not a purchase: no ledger entry is written, and if no SMS arrives the whole amount comes back untouched. Only a delivered code turns the hold into a real charge, at which point one transaction appears. Read charged on the activation to know which side of that line you are on. Rentals do not work this way: they are charged in full upfront and are not refundable.

Discounts. Premium and Premium Pro discounts apply to eSIM pricing and are baked into the price you see in /esim/packages. Airtime and data are sold close to cost and are excluded from the discount.

Read the price you pay. The price field in a purchase response is the exact NGN amount debited. For airtime it is higher than the amount the recipient receives, because the markup is added on top. Reconcile against price, never against your requested amount.

Rate limits

Limits are per key, per rolling 60 seconds, and scale with your plan:

  • Free — 60 requests / minute
  • Premium — 600 requests / minute
  • Premium Pro — 2,000 requests / minute

Exceeding the limit returns 429 with a message naming your ceiling. The limit counts every request, including ones that fail validation, so a retry loop on a 400 will burn through it. Issue separate keys for separate workloads if a batch job risks starving your live traffic.

Errors

Errors return a non-2xx status and a JSON body with a single error string meant to be shown to a developer, not an end user.

{ "error": "Insufficient wallet balance." }
StatusMeaningWhat to do
400The request was malformed or a parameter failed validation.Read the error field and fix the payload. Retrying unchanged will not help.
401Missing, malformed, revoked or unknown API key.Check the Authorization header. Revoked keys never come back — issue a new one.
402Insufficient wallet balance for this purchase.Top up the wallet, then retry. Nothing was charged.
409Not enough marketplace stock, or a request with this Idempotency-Key is still in flight. The error message says which.For stock, re-read the catalog and lower the quantity. For a key in flight, wait and retry the same key — do not switch keys.
422This Idempotency-Key was already used with a different request body.One key belongs to one purchase. Generate a new key for a new order.
429Per-key rate limit exceeded.Back off and retry. The message names your ceiling.
500The upstream order succeeded but recording it failed.Your wallet was restored. Contact support before retrying — the goods may already have been delivered.
502The upstream provider failed or timed out.Your wallet was restored and nothing was delivered. Safe to retry.
503An order could not be recorded and was rolled back in full — a marketplace order, or a number whose hold was released and whose line was handed back.Nothing was charged and no stock was consumed. Safe to retry, and the Idempotency-Key is freed so you can reuse it.

Retries & idempotency

Send an Idempotency-Key header on every purchase. Generate one UUID per order you intend to place, and reuse that same value on every retry of it. The first request does the work and stores its result; any repeat replays that stored response instead of buying again.

KEY=$(uuidgen)

# Safe to run twice: the second call replays the first result.
curl -X POST https://your-onedex-domain/api/v1/bills/airtime \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "network": "mtn", "phone": "08012345678", "amount": 500 }'

This is what makes a timeout recoverable. Without the header a request that succeeded but never reached you is indistinguishable from one that never ran, and retrying charges twice.

  • A replayed response carries Idempotent-Replay: true and the original status code and body — including the eSIM QR or order id.
  • Reusing a key with a different body returns 422. That is a bug guard: one key belongs to one purchase.
  • A 409 means your first attempt is still running. Wait and retry the same key to read its result — do not switch keys.
  • Keys are scoped to your account and expire after 24 hours.

Beyond that, the same defensive habits still apply:

  • 429 and 502 guarantee you were not charged and are always safe to retry, key or no key.
  • A 500 means the order may have gone through. With a key, retrying replays the 500 rather than buying again — contact support rather than switching to a fresh key.
  • Use generous client timeouts — 60 seconds or more. Purchases wait on an upstream provider, and cutting the connection early does not cancel the order.
  • Store the returned id and reference against your own order record as soon as you get them. They are your only handle on the purchase afterwards.

Endpoint reference

GET/api/v1/balancefree to call

Wallet balance

Returns the spendable wallet balance for the key's owner. Poll this before a purchase if you want to fail early rather than handle a 402.

curl https://your-onedex-domain/api/v1/balance \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "balance": 12500.00,
  "currency": "NGN"
}
  • Referral commission sits in a separate balance and is not included here.
GET/api/v1/esim/packagesfree to call

List eSIM packages

The full eSIM catalog, grouped by country. Prices are already converted to NGN and already include your Premium/Pro discount, so the number you see is the number you will be charged.

curl https://your-onedex-domain/api/v1/esim/packages \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "countries": [
    {
      "country_code": "US",
      "title": "United States",
      "packages": [
        {
          "id": "pkg_us_1gb_7d",
          "title": "US 1GB · 7 days",
          "data": "1GB",
          "days": 7,
          "unlimited": false,
          "price": 3200,
          "currency": "NGN"
        }
      ]
    }
  ]
}
  • Requires a key even though it only reads the catalog — pricing is per-account.
  • Package IDs are not stable forever. Resolve them from this endpoint rather than hardcoding.
POST/api/v1/esim/orders

Buy an eSIM

Purchases a package and returns the issued eSIM with its QR code and activation details. The price is debited from your wallet before the upstream order is placed, and automatically reversed if issuing fails.

Charges: The package price from /esim/packages.

FieldTypeDescription
package_idrequiredstringAn id from /esim/packages. packageId is accepted as an alias.
curl -X POST https://your-onedex-domain/api/v1/esim/orders \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "package_id": "pkg_us_1gb_7d" }'

Response

{
  "id": "b0f2…",
  "price": 3200,
  "currency": "NGN",
  "iccid": "89014…",
  "qrcode": "LPA:1$…",
  "qrcode_url": "https://…",
  "lpa": "LPA:1$…",
  "matching_id": "…",
  "data": "1GB",
  "validity_days": 7
}
  • Render qrcode_url for the customer to scan, or offer lpa for manual entry on devices that support it.
  • A 502 means the eSIM was not issued and your wallet was already restored.
GET/api/v1/bills/data-plansfree to call

List data plans

Every Nigerian data bundle currently on sale across MTN, Airtel, Glo and 9mobile, with the price you will be charged.

curl https://your-onedex-domain/api/v1/bills/data-plans \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "plans": [
    {
      "package_code": "mtn_sme_1gb",
      "network": "mtn",
      "data": "1GB",
      "type": "SME",
      "validity": "30 days",
      "price": 750,
      "currency": "NGN"
    }
  ]
}
  • Availability changes throughout the day. Fetch this immediately before ordering rather than caching it for hours.
POST/api/v1/bills/airtime

Buy airtime

Tops up a Nigerian number with airtime. amount is the face value the recipient receives; price in the response is what left your wallet.

Charges: Face value plus the airtime markup. Premium discounts do not apply.

FieldTypeDescription
networkrequiredstringOne of mtn, airtel, glo, 9mobile.
phonerequiredstringRecipient number. 080…, +234… and 234… are all normalized for you.
amountrequirednumberFace value in NGN, 50–50,000. Decimals are truncated.
curl -X POST https://your-onedex-domain/api/v1/bills/airtime \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "network": "mtn", "phone": "08012345678", "amount": 500 }'

Response

{
  "id": "a91c…",
  "price": 505,
  "currency": "NGN",
  "reference": "…",
  "status": "success"
}
  • price is the wallet debit and is normally higher than amount. Never assume the two match.
POST/api/v1/bills/data

Buy a data bundle

Delivers a data bundle to a Nigerian number. The network is inferred from the package code, so you do not pass one.

Charges: The plan price from /bills/data-plans. Premium discounts do not apply.

FieldTypeDescription
phonerequiredstringRecipient number, normalized as above.
package_coderequiredstringA package_code from /bills/data-plans. packageCode is accepted as an alias.
curl -X POST https://your-onedex-domain/api/v1/bills/data \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "phone": "08012345678", "package_code": "mtn_sme_1gb" }'

Response

{
  "id": "77ab…",
  "price": 750,
  "currency": "NGN",
  "reference": "…",
  "status": "success"
}
GET/api/v1/store/productsfree to call

List marketplace products

The marketplace catalog with live stock counts. Prices already include your Premium/Pro discount. Read-only — see the coverage table for why buying stays in the dashboard.

FieldTypeDescription
categorystringCase-insensitive exact category filter, e.g. ?category=Accounts.
curl https://your-onedex-domain/api/v1/store/products \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "products": [
    {
      "id": "3f9c…",
      "name": "Aged Gmail (US)",
      "description": "Created 2019, recovery email included",
      "category": "Accounts",
      "image_url": null,
      "price": 1800,
      "currency": "NGN",
      "stock": 14
    }
  ]
}
  • stock is the number of unsold units right now and moves as other buyers purchase. Treat it as a hint, not a reservation.
  • The item being sold (the credential itself) is never returned here — it is claimed at checkout.
POST/api/v1/store/orders

Buy marketplace products

Claims stock, debits your wallet and returns the purchased items in the response body. Unlike eSIM and bills, nothing here touches an external provider — so any non-2xx means the whole thing rolled back and you were not charged.

Charges: The product price × quantity, at your discounted rate.

FieldTypeDescription
product_idrequiredstringAn id from /store/products. productId is accepted as an alias.
quantityinteger1–50, defaults to 1. Each unit is a separate item in the response.
curl -X POST https://your-onedex-domain/api/v1/store/orders \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "product_id": "3f9c…", "quantity": 2 }'

Response

{
  "id": "8d21…",
  "product_id": "3f9c…",
  "product_name": "Aged Gmail (US)",
  "quantity": 2,
  "unit_price": 1800,
  "price": 3600,
  "currency": "NGN",
  "status": "delivered",
  "items": [
    { "id": "aa10…", "value": "user@gmail.com:hunter2" },
    { "id": "bb27…", "value": "other@gmail.com:swordfish" }
  ]
}
  • items is the delivery. Persist it on receipt — re-reading it later means querying your order history, and a lost response is only recoverable if you sent an Idempotency-Key.
  • A 409 means stock ran out between your catalog read and this call (the message names how many were actually available), or that a request with the same Idempotency-Key is still running.
  • Partial fills never happen: either every unit is claimed or none are.
GET/api/v1/verify/countriesfree to call

List number countries

Countries with verification numbers available. Use an id from here as the country argument to /verify/services.

FieldTypeDescription
serverstringserver-1 or server-2. Defaults to the primary. An unrecognised value is rejected, never silently defaulted.
curl https://your-onedex-domain/api/v1/verify/countries \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "countries": [
    {
      "id": "england",
      "name": "United Kingdom",
      "has_services": true,
      "operators": ["any", "ee", "three"]
    }
  ]
}
  • server-1 and server-2 are independent: each has its own countries, services, stock and prices. A country sold out on one is often available on the other, so fall back rather than giving up.
  • Country ids are not shared between servers. Resolve them from the same server you intend to buy from.
GET/api/v1/verify/servicesfree to call

List number services

Services available in a country with live stock and NGN prices including your discount. Pass a code from here as the service when buying a number.

FieldTypeDescription
countryrequiredstringA country id from /verify/countries.
operatorsstringComma-separated operator filter, or 'any'.
serverstringserver-1 or server-2. Defaults to the primary. An unrecognised value is rejected, never silently defaulted.
curl "https://your-onedex-domain/api/v1/verify/services?country=england" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "services": [
    {
      "code": "wa",
      "name": "WhatsApp",
      "count": 240,
      "price": 950,
      "currency": "NGN"
    }
  ]
}
  • The price here is indicative. It comes from the upstream's aggregate catalog and is usually a little above what you are actually charged, because buying takes the cheapest operator. Read /verify/servers for the exact figure — its cheapest option is what a purchase gets held at.
  • Prices track the upstream and the FX rate, so they move. Read them immediately before you show them.
  • A 502 means the upstream failed — distinct from a successful call returning an empty list.
GET/api/v1/verify/serversfree to call

List operator options

Per-option prices for a country and service, cheapest first. Operators are relabelled Option 1/2/3 — pass the operator value straight back when you buy.

FieldTypeDescription
countryrequiredstringA country id from /verify/countries.
servicerequiredstringA service code from /verify/services.
serverstringserver-1 or server-2. Defaults to the primary. An unrecognised value is rejected, never silently defaulted.
curl "https://your-onedex-domain/api/v1/verify/servers?country=england&service=wa" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "servers": [
    {
      "operator": "three",
      "label": "Option 1",
      "price": 950,
      "currency": "NGN",
      "count": 87,
      "successRate": 21.43,
      "successRateRecent": 25.42,
      "rateStatus": "live"
    }
  ]
}
  • Skip this entirely and omit operators when buying — you get the cheapest option.
  • successRate is the operator's current delivery percentage — the share of numbers that recently received an SMS. It is a short window and moves through the day, so read it at the time you buy rather than caching it. It is null when no figure is reported, which is not the same as 0.
  • Branch on rateStatus rather than comparing successRate to 0. It is one of live (delivering in the latest window), quiet (nothing in the latest window but delivering over a longer one), not-delivering (every window the provider reports is zero) or unknown (no figure at all). About a third of in-stock options read 0 in the latest window while still delivering, so treating 0 as broken will make you skip usable numbers. successRateRecent is the best figure across every window, and is the number behind that verdict.
POST/api/v1/verify/activations

Buy a one-time number

Buys a number and holds its price in escrow. You are not charged yet: the hold only becomes a real debit when a code arrives. Poll the number next, then either read its code or cancel for the hold back.

Charges: Held, not charged. The service price from /verify/services, debited only on delivery.

FieldTypeDescription
servicerequiredstringA service code from /verify/services.
countryrequiredstringA country id from /verify/countries.
operatorsstring[] | stringPreferred operator(s) from /verify/servers, as an array or comma-separated. Omit for the cheapest.
serverstringserver-1 or server-2. Defaults to the primary. Each carries its own stock and prices.
service_namestringOptional label stored with the order and shown in history.
country_namestringOptional label stored with the order and shown in history.
curl -X POST https://your-onedex-domain/api/v1/verify/activations \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "service": "wa", "country": "england" }'

Response

{
  "id": "3f9c…",
  "status": "waiting",
  "phone_number": "447700900123",
  "service": "wa",
  "country": "england",
  "server": "server-1",
  "price": 950,
  "currency": "NGN",
  "charged": false,
  "otp_code": null,
  "sms_text": null,
  "expires_at": "2026-08-22T10:45:00Z",
  "resends": [],
  "poll_after_ms": 5000
}
  • charged is false and stays false until an SMS lands. Until then the money is held, not spent — it never appears in your transaction history.
  • Always send an Idempotency-Key. A timed-out purchase replayed without one buys a second number.
  • Every failure here unwinds completely — the hold is released and the number handed back — so retrying is safe, including with the same Idempotency-Key.
GET/api/v1/verify/activations/{id}free to call

Poll a number for its code

Reading a number is what advances it. When a code lands, this call settles the escrow into a real charge and returns the code; when the window closes it releases the hold. Keep calling while poll_after_ms is non-null.

curl https://your-onedex-domain/api/v1/verify/activations/3f9c… \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "3f9c…",
  "status": "received",
  "phone_number": "447700900123",
  "price": 950,
  "currency": "NGN",
  "charged": true,
  "otp_code": "482913",
  "sms_text": "482913 is your WhatsApp code",
  "expires_at": "2026-08-22T10:45:00Z",
  "resends": [],
  "poll_after_ms": null
}
  • status runs waiting → received (charged), or → cancelled/expired (hold released, nothing charged).
  • An SMS that carries no parseable code still counts as delivery: otp_code is null, sms_text has the body, and you are charged. Read sms_text as a fallback.
  • Free to call, but every poll counts against your rate limit. Honour poll_after_ms instead of polling flat out.
POST/api/v1/verify/activations/{id}/cancelfree to call

Cancel a waiting number

Hands the number back and releases the whole hold. Only numbers still in waiting can be cancelled — once a code has landed the purchase is settled and final.

curl -X POST https://your-onedex-domain/api/v1/verify/activations/3f9c…/cancel \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "3f9c…",
  "status": "cancelled",
  "refunded": 950,
  "currency": "NGN"
}
  • refunded is a released hold, not a credit. Because the money was never spent, no transaction is written either way.
  • A number left to expire releases its hold on its own — cancelling just gets it back sooner.
POST/api/v1/verify/activations/{id}/resend

Request another SMS

Asks the same number for another SMS. Free while the first code is still outstanding; a new purchase at the same price once one has already landed.

Charges: Nothing while waiting. The original price once a code has settled.

curl -X POST https://your-onedex-domain/api/v1/verify/activations/3f9c…/resend \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)"

Response

{
  "activation_id": "3f9c…",
  "charged": true,
  "price": 950,
  "currency": "NGN",
  "resend_id": "c14a…"
}
  • Check charged to know whether this cost anything.
  • A paid re-request gets its own hold and its own window. Poll the activation and watch it resolve under resends.
  • Not every upstream can resend. Those that cannot return 400 — cancel for the hold back and buy a new number instead.
GET/api/v1/verify/activationsfree to call

List recent numbers

Your recent numbers, newest first. This is the recovery path when a purchase response is lost on the wire: the number is here, so resume polling it rather than buying another.

FieldTypeDescription
limitinteger1–100. Defaults to 20.
statusstringFilter to waiting, received, cancelled or expired.
curl "https://your-onedex-domain/api/v1/verify/activations?status=waiting" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "activations": [
    {
      "id": "3f9c…",
      "status": "waiting",
      "phone_number": "447700900123",
      "price": 950,
      "currency": "NGN",
      "charged": false,
      "poll_after_ms": 5000
    }
  ]
}
  • Calling this also releases any holds whose windows lapsed while nothing was polling them.
GET/api/v1/verify/rentals/catalogfree to call

List rentable countries

Countries available for long-term rental with their day-durations, priced with your discount. Use a country's id as line_id when renting.

curl https://your-onedex-domain/api/v1/verify/rentals/catalog \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "countries": [
    {
      "id": "14",
      "name": "United Kingdom",
      "country_short": "GB",
      "region": "Europe",
      "count": 42,
      "durations": [
        { "days": 7, "price": 8500, "currency": "NGN" },
        { "days": 30, "price": 24000, "currency": "NGN" }
      ]
    }
  ]
}
  • Rent only a duration this endpoint offers for that country.
POST/api/v1/verify/rentals

Rent a long-term number

Rents a number for a fixed number of days. Unlike a one-time number there is no escrow — the full price is debited immediately and is not refundable.

Charges: The duration price from /verify/rentals/catalog, debited in full.

FieldTypeDescription
line_idrequiredstringA country id from /verify/rentals/catalog.
daysrequiredintegerA duration offered for that country.
country_namestringOptional label stored with the rental.
country_shortstringOptional label stored with the rental.
curl -X POST https://your-onedex-domain/api/v1/verify/rentals \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "line_id": "14", "days": 7 }'

Response

{
  "id": "9ab1…",
  "status": "active",
  "phone_number": "447700900456",
  "country": "GB",
  "days": 7,
  "price": 8500,
  "currency": "NGN",
  "sms_count": 0,
  "messages": [],
  "expires_at": "2026-08-29T09:00:00Z",
  "poll_after_ms": 5000
}
  • The line is yours for the whole window whether or not any SMS arrives. There is no cancel-for-refund here — that is what one-time numbers are for.
  • Send an Idempotency-Key. A replayed rental is an expensive mistake.
GET/api/v1/verify/rentals/{id}free to call

Read a rental inbox

Returns the rental with every SMS received during its window, oldest last, so you can match codes to whatever you sent.

curl https://your-onedex-domain/api/v1/verify/rentals/9ab1… \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "9ab1…",
  "status": "active",
  "phone_number": "447700900456",
  "price": 8500,
  "currency": "NGN",
  "sms_count": 2,
  "messages": [
    {
      "sender": "WhatsApp",
      "text": "482913 is your code",
      "code": "482913",
      "received_at": "2026-08-22T09:12:00Z"
    }
  ],
  "expires_at": "2026-08-29T09:00:00Z",
  "poll_after_ms": 5000
}
  • A rental keeps receiving until its window closes, so the inbox grows. Track sms_count to spot new arrivals cheaply.
GET/api/v1/verify/rentalsfree to call

List recent rentals

Your recent rentals, newest first — the recovery path for a purchase whose response was lost. Inboxes are not included; read one rental to fetch its messages.

FieldTypeDescription
limitinteger1–100. Defaults to 20.
statusstringFilter to active, finished or expired.
curl "https://your-onedex-domain/api/v1/verify/rentals?status=active" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "rentals": [
    {
      "id": "9ab1…",
      "status": "active",
      "phone_number": "447700900456",
      "days": 7,
      "price": 8500,
      "currency": "NGN",
      "sms_count": 2,
      "messages": [],
      "expires_at": "2026-08-29T09:00:00Z",
      "poll_after_ms": 5000
    }
  ]
}
POST/api/v1/verify/rentals/{id}/finishfree to call

End a rental early

Releases the line upstream and closes the rental. There is no refund — the window was paid for at purchase.

curl -X POST https://your-onedex-domain/api/v1/verify/rentals/9ab1…/finish \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "9ab1…",
  "status": "finished",
  "refunded": 0,
  "currency": "NGN"
}
  • Use this when you are done with a number, not to undo a rental. refunded is always 0.
GET/api/v1/checkmark/listingsfree to call

List checkmark listings

Active account-verification listings, grouped-friendly (sorted by platform then price) and priced with your discount applied.

curl https://your-onedex-domain/api/v1/checkmark/listings \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "listings": [
    {
      "id": "a71e…",
      "platform": "Instagram",
      "title": "Blue badge — creator",
      "description": "7–14 days, no follower minimum",
      "image_url": null,
      "verification_type": "username",
      "price": 145000,
      "currency": "NGN"
    }
  ]
}
  • verification_type is 'credentials' when the listing needs account login details. Those are only ever collected in the dashboard.
GET/api/v1/topup/listingsfree to call

List subscription listings

Active subscription and top-up listings, sorted by platform then price, with your discount applied. An id from here is what a request is placed against.

curl https://your-onedex-domain/api/v1/topup/listings \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "listings": [
    {
      "id": "c02b…",
      "platform": "Netflix",
      "title": "1 month · Premium 4K",
      "description": "Private profile on a shared plan",
      "image_url": null,
      "price": 6500,
      "currency": "NGN"
    }
  ]
}
POST/api/v1/topup/orders

Order a subscription top-up

Places a request against a listing and holds its price in escrow. You are not charged yet — these are fulfilled by hand, so the hold becomes a real debit only when the request completes, and goes back in full if it fails. Poll the request next.

Charges: Held, not charged. The listing price at your discounted rate, debited only on completion.

FieldTypeDescription
listing_idrequiredstringAn id from /topup/listings.
accountrequiredstringThe account to top up — email, phone, username or player id. The listing's instructions field says which one it needs.
contactrequiredstringEmail, phone or WhatsApp we can reach the buyer on if the request needs a question answered.
curl -X POST https://your-onedex-domain/api/v1/topup/orders \n  -H "Authorization: Bearer odx_live_..." \n  -H "Idempotency-Key: $(uuidgen)" \n  -H "Content-Type: application/json" \n  -d '{ "listing_id": "c02b…", "account": "buyer@example.com", "contact": "+2348012345678" }'

Response

{
  "id": "4d7f…",
  "status": "processing",
  "listing_id": "c02b…",
  "platform": "Netflix",
  "title": "1 month · Premium 4K",
  "account": "buyer@example.com",
  "contact": "+2348012345678",
  "price": 6500,
  "currency": "NGN",
  "charged": false,
  "notes": null,
  "created_at": "2026-08-24T09:00:00Z",
  "poll_after_ms": 300000
}
  • Fulfilment is manual and takes hours, not seconds. Treat a 200 here as an accepted request, not a delivered product.
  • Send an Idempotency-Key. A replayed request holds the price a second time.
GET/api/v1/topup/orders/{id}free to call

Poll a subscription request

Reads one request. Unlike a number, this call moves no money — it reports where a human got to. 'completed' with charged: true is the end of the happy path; 'failed' means the hold went back untouched and notes says why.

curl https://your-onedex-domain/api/v1/topup/orders/4d7f… \n  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "4d7f…",
  "status": "completed",
  "platform": "Netflix",
  "title": "1 month · Premium 4K",
  "account": "buyer@example.com",
  "price": 6500,
  "currency": "NGN",
  "charged": true,
  "notes": "Activated on the account.",
  "created_at": "2026-08-24T09:00:00Z",
  "updated_at": "2026-08-24T14:20:00Z",
  "poll_after_ms": null
}
  • Poll on poll_after_ms (5 minutes) rather than tightly — nothing changes until an admin acts.
GET/api/v1/topup/ordersfree to call

List subscription requests

Your recent subscription requests, newest first. This is the recovery path when a submission response is lost on the wire: the request is here, so resume polling it rather than paying twice.

FieldTypeDescription
limitinteger1–100. Defaults to 20.
statusstringFilter to processing, completed or failed.
curl "https://your-onedex-domain/api/v1/topup/orders?status=processing" \n  -H "Authorization: Bearer odx_live_..."

Response

{
  "orders": [
    {
      "id": "4d7f…",
      "status": "processing",
      "platform": "Netflix",
      "title": "1 month · Premium 4K",
      "account": "buyer@example.com",
      "price": 6500,
      "currency": "NGN",
      "charged": false,
      "notes": null,
      "created_at": "2026-08-24T09:00:00Z",
      "poll_after_ms": 300000
    }
  ]
}
GET/api/v1/boost/serversfree to call

List boost servers

The panels available to your account. Each has its own catalog, prices and minimums, so a service id is only valid on the server you read it from.

curl https://your-onedex-domain/api/v1/boost/servers \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "servers": [
    { "id": "server-1", "label": "Server 1" },
    { "id": "server-2", "label": "Server 2" }
  ]
}
GET/api/v1/boost/servicesfree to call

List boost services

One server's catalog, priced per 1000 units with your discount applied. An order costs quantity ÷ 1000 × rate_per_1000. Each entry says what its type needs beyond a link.

FieldTypeDescription
serverstringserver-1 or server-2. Defaults to the primary.
categorystringExact category match, e.g. 'Instagram Followers'.
searchstringCase-insensitive substring match on the service name.
curl "https://your-onedex-domain/api/v1/boost/services?server=server-1&search=instagram+followers" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "services": [
    {
      "id": 1423,
      "name": "Instagram Followers | Real | 30 days refill",
      "category": "Instagram Followers",
      "type": "Default",
      "min": 50,
      "max": 100000,
      "rate_per_1000": 1850.5,
      "currency": "NGN",
      "requires": [],
      "quantity_from": null
    }
  ]
}
  • A catalog runs to thousands of entries. Filter it rather than pulling the whole list before every order.
  • requires lists the extra fields the service's type needs — comments, usernames, hashtag, username or answer_number. When quantity_from is set, the quantity is the number of lines in that list and must not be sent.
POST/api/v1/boost/orders

Place a boost order

Debits your wallet in full and submits the order to the panel. There is no escrow: if the panel rejects it the debit is reversed and nothing is recorded, but once accepted the money is spent and only returns as a refund for undelivered units.

Charges: quantity ÷ 1000 × rate_per_1000, at your discounted rate, debited upfront.

FieldTypeDescription
service_idrequiredintegerAn id from /boost/services on the same server.
linkrequiredstringThe profile, post or video URL to deliver to.
quantityintegerUnits to deliver, within the service's min and max. Omit for services with a quantity_from list.
serverstringserver-1 or server-2. Defaults to the primary.
comments / usernamesarray | stringFor Custom Comments and custom-list Mentions services: one entry per line or array item. The quantity is the count.
hashtag / username / answer_numberstringFor hashtag Mentions, follower Mentions and Poll services respectively — whichever the service's requires array names.
curl -X POST https://your-onedex-domain/api/v1/boost/orders \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "service_id": 1423, "link": "https://instagram.com/onedex", "quantity": 1000 }'

Response

{
  "id": "b71c…",
  "status": "Pending",
  "server": "server-1",
  "service_id": 1423,
  "service_name": "Instagram Followers | Real | 30 days refill",
  "category": "Instagram Followers",
  "link": "https://instagram.com/onedex",
  "quantity": 1000,
  "charge": 1850.5,
  "refunded": 0,
  "currency": "NGN",
  "start_count": null,
  "remains": null,
  "created_at": "2026-08-24T09:00:00Z",
  "poll_after_ms": 60000
}
  • Delivery is never instant. A 200 means accepted and running, not delivered — poll the order for progress.
  • Send an Idempotency-Key. A replayed order is a second paid order.
  • A 400 covers both your own validation errors and a panel refusal. Either way nothing was charged: a debit taken before a refusal is reversed in full.
GET/api/v1/boost/orders/{id}free to call

Poll a boost order

Pulls the live status from the panel, stores it, and settles any refund the order is owed. Polling is what puts money back in your wallet when an order is cancelled or ends Partial — it can never refund twice.

curl https://your-onedex-domain/api/v1/boost/orders/b71c… \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "b71c…",
  "status": "Partial",
  "server": "server-1",
  "quantity": 1000,
  "charge": 1850.5,
  "refunded": 370.1,
  "currency": "NGN",
  "start_count": 4210,
  "remains": 200,
  "updated_at": "2026-08-24T11:40:00Z",
  "poll_after_ms": 60000
}
  • charge − refunded is what the order actually cost. refunded is a running total, not this call's delta.
  • poll_after_ms is null once the order is Completed or Canceled. Partial stays pollable because remains can still be revised.
GET/api/v1/boost/ordersfree to call

List boost orders

Your recent boost orders, newest first — the recovery path when an order response is lost on the wire. Statuses are whatever the panel last reported; this read does not poll upstream.

FieldTypeDescription
limitinteger1–100. Defaults to 20.
statusstringExact status match, e.g. Completed, In progress, Partial, Canceled.
curl "https://your-onedex-domain/api/v1/boost/orders?status=In+progress" \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "orders": [
    {
      "id": "b71c…",
      "status": "In progress",
      "server": "server-1",
      "service_name": "Instagram Followers | Real | 30 days refill",
      "link": "https://instagram.com/onedex",
      "quantity": 1000,
      "charge": 1850.5,
      "refunded": 0,
      "currency": "NGN",
      "remains": 640,
      "poll_after_ms": 60000
    }
  ]
}
POST/api/v1/boost/orders/{id}/cancelfree to call

Cancel a boost order

Asks the panel to cancel. Cancellation is the panel's call, not ours: a 409 means it refused — already in progress, or a service that can't be cancelled — and the order keeps running with nothing refunded.

curl -X POST https://your-onedex-domain/api/v1/boost/orders/b71c…/cancel \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "b71c…",
  "status": "Canceled",
  "refund_issued": true,
  "refund_amount": 1850.5,
  "currency": "NGN"
}
  • refund_issued false is normal: the panel accepted the cancellation but hasn't processed it yet. Keep polling the order — the refund settles there.
POST/api/v1/boost/orders/{id}/refillfree to call

Request a refill

Asks the panel to top a delivered order back up after drops. Free — no wallet movement either way.

curl -X POST https://your-onedex-domain/api/v1/boost/orders/b71c…/refill \
  -H "Authorization: Bearer odx_live_..."

Response

{
  "id": "b71c…",
  "refill_id": 88421,
  "status": "requested"
}
  • Only some services offer refills, and only inside their window. A 409 with the panel's reason is a normal outcome, not an error to retry.

Integration guides

Selling eSIMs

Catalog first, order second — never hardcode a package id, because availability and pricing move.

# 1. Fetch the catalog and let the customer choose
curl https://your-onedex-domain/api/v1/esim/packages -H "Authorization: Bearer odx_live_..."

# 2. Order the chosen package id
curl -X POST https://your-onedex-domain/api/v1/esim/orders \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "package_id": "pkg_us_1gb_7d" }'

# 3. Show qrcode_url to the customer, or hand them lpa for manual install.
#    Store id + iccid against your own order record.

Selling airtime

The simplest flow in the API — one call, no catalog lookup. Validate the recipient number yourself before spending, because a well-formed number that belongs to the wrong network still costs you.

curl -X POST https://your-onedex-domain/api/v1/bills/airtime \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "network": "mtn", "phone": "08012345678", "amount": 500 }'

# The recipient gets ₦500. Your wallet is debited "price", which is higher.

Selling data bundles

Fetch plans immediately before ordering. A package_code you cached this morning may be gone by the afternoon, which returns 400.

# 1. Pull the live plan list
curl https://your-onedex-domain/api/v1/bills/data-plans -H "Authorization: Bearer odx_live_..."

# 2. Order by package_code — the network is inferred from it
curl -X POST https://your-onedex-domain/api/v1/bills/data \
  -H "Authorization: Bearer odx_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "phone": "08012345678", "package_code": "mtn_sme_1gb" }'

Selling verification numbers

The one flow in the API that is deliberately more than one request. A number is worthless until an SMS actually lands, so the price is held rather than charged: you poll until a code arrives (charged) or hand the number back (free). Build your own screen around this loop — the three calls are the whole contract.

# 1. Buy — money is held, not spent. Save the id.
curl -X POST https://your-onedex-domain/api/v1/verify/activations \
  -H "Authorization: Bearer odx_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "service": "wa", "country": "england" }'

# 2. Poll every poll_after_ms until status leaves "waiting"
curl https://your-onedex-domain/api/v1/verify/activations/3f9c… \
  -H "Authorization: Bearer odx_live_..."

# 3a. status "received"  → otp_code / sms_text are filled in, you are charged
# 3b. no code in time     → cancel and the whole hold comes back
curl -X POST https://your-onedex-domain/api/v1/verify/activations/3f9c…/cancel \
  -H "Authorization: Bearer odx_live_..."
// The same loop in JavaScript, using the client below.
async function buyNumber(service, country, { giveUpAfterMs = 180_000 } = {}) {
  const activation = await onedex("/verify/activations", { service, country });
  const deadline = Date.now() + giveUpAfterMs;

  while (Date.now() < deadline) {
    const state = await onedex(`/verify/activations/${activation.id}`);

    // A code, or an SMS body we could not parse a code out of — both are
    // deliveries, and both mean the hold has become a real charge.
    if (state.status === "received") return state;

    // The window closed on its own; the hold was already released.
    if (state.status !== "waiting") return null;

    await new Promise((r) => setTimeout(r, state.poll_after_ms ?? 5000));
  }

  // Out of patience — hand the number back and get the hold released.
  await onedex(`/verify/activations/${activation.id}/cancel`, {});
  return null;
}

Two things worth designing for. Poll on poll_after_ms rather than a loop of your own choosing — a free key gets 60 requests a minute, so a tight loop watching a handful of numbers will hit 429 long before the numbers do anything. And if your process dies mid-purchase, do not buy again: GET /verify/activations?status=waiting lists the numbers you are already holding money against.

For a number you need for days rather than minutes, rent one instead. Rentals are charged in full upfront and never refunded, so treat POST /verify/rentals as final at the moment it returns.

Keeping the wallet funded

There is no funding endpoint — top-ups happen in the dashboard. For unattended systems, poll /balance on a schedule and alert your team below a threshold that covers your busiest hour. A wallet that empties mid-day turns every purchase into a 402.

JavaScript & Python

No SDK is needed — the API is plain HTTP and JSON. These are complete, runnable clients.

JavaScript (Node 18+)

const BASE = "https://your-onedex-domain/api/v1";
const KEY = process.env.ONEDEX_API_KEY;

async function onedex(path, body) {
  const res = await fetch(BASE + path, {
    method: body ? "POST" : "GET",
    headers: {
      Authorization: `Bearer ${KEY}`,
      ...(body && { "Content-Type": "application/json" }),
    },
    ...(body && { body: JSON.stringify(body) }),
    signal: AbortSignal.timeout(60_000),
  });

  const data = await res.json();
  if (!res.ok) {
    const err = new Error(data.error ?? `HTTP ${res.status}`);
    err.status = res.status;
    // 429 and 502 guarantee you were not charged — safe to retry.
    err.retryable = res.status === 429 || res.status === 502;
    throw err;
  }
  return data;
}

const { balance } = await onedex("/balance");
console.log("Wallet:", balance);

const order = await onedex("/bills/airtime", {
  network: "mtn",
  phone: "08012345678",
  amount: 500,
});
console.log("Charged:", order.price, "ref:", order.reference);

Python 3 (requests)

import os, requests

BASE = "https://your-onedex-domain/api/v1"
KEY = os.environ["ONEDEX_API_KEY"]


class OnedexError(Exception):
    def __init__(self, message, status):
        super().__init__(message)
        self.status = status
        # 429 and 502 guarantee you were not charged — safe to retry.
        self.retryable = status in (429, 502)


def onedex(path, body=None):
    res = requests.request(
        "POST" if body else "GET",
        BASE + path,
        headers={"Authorization": f"Bearer {KEY}"},
        json=body,
        timeout=60,
    )
    data = res.json()
    if not res.ok:
        raise OnedexError(data.get("error", f"HTTP {res.status_code}"), res.status_code)
    return data


print("Wallet:", onedex("/balance")["balance"])

order = onedex("/bills/data", {
    "phone": "08012345678",
    "package_code": "mtn_sme_1gb",
})
print("Charged:", order["price"], "ref:", order["reference"])

OpenAPI spec

The whole API is published as an OpenAPI 3.1 document. Import it into Postman, Insomnia or Bruno, or run your generator of choice against it — no key required to read it.

https://your-onedex-domain/api/v1/openapi.json

View the spec

Get your API key

Create an account and generate a key in seconds.

Get started