The catalogue tree

Walk the catalogue one scoped level per call — platforms, then countries, then providers, then options — without an API key, and feed the ids straight into an order.

Embed
Preview of the card
<a href="https://numestri.com/docs/the-catalogue-tree"><img src="https://numestri.com/docs/_api/public/orgs/25229885-9620-4c08-bd2e-a0d0d74ac8b6/spaces/ef21e0c3-966d-4b03-b6d2-e66d449835d4/pages/b4c72ad2-0040-44af-b478-dee98cb1e309/social-card.svg?v=3e7ae33eec8b" alt="The catalogue tree" width="600" height="315"></a>

The catalogue is a tree. Each call returns only the level under the choice you made, so you never download the whole market to find one price.

/catalog/platforms                                                         every platform in stock
/catalog/platforms/{platform}/countries                                    where that platform is sold
/catalog/platforms/{platform}/countries/{country}/providers                who sells it there
/catalog/platforms/{platform}/countries/{country}/providers/{provider}/options   what each sells it from

Every id along the way is exact and is what /purchase/sms takes.

No key needed

The whole tree is public. Call it without a key and you get the public list prices, limited to 10 requests per second per IP address. Send your key and every price is your account's own — what an order will actually charge you — under your account's limit. A key that is wrong is refused with 403 rather than ignored. See Authentication.

curl https://numestri.com/api/v1/catalog/platforms
{
  "success": 1,
  "prices_at": "2026-10-14T01:40:00.000Z",
  "count": 1326,
  "next": "/api/v1/catalog/platforms/{platform}/countries",
  "data": [
    { "id": "discord", "name": "Discord", "countries": 12, "from_price": "0.07", "from_price_cents": 7 }
  ]
}

Field

Meaning

id

The platform id: {platform} in the next path, service in an order.

countries

How many countries it is in stock in.

from_price / from_price_cents

Its cheapest number, anywhere.

Ids: platform, country, provider, option

Id

Looks like

Stable?

Use it as

Platform

telegram, whatsapp

Yes

service

Country

US, GB (upper case)

Yes

country

Provider

prv_ + 10 characters, e.g. prv_k3Jd9QmXa2

Yes — the same provider keeps its handle across platforms, countries and time

provider

Option

8–32 letters, digits, - or _, e.g. k3Jd9QmXa2Lp0cWe

For as long as that offer exists

option

Both handles are opaque. A provider handle says nothing about who is behind it, and an option id is specific to one platform and country — the same offer elsewhere has a different id — so don't parse them or build them yourself; copy them from the catalogue. An option that no longer exists is refused at order time with code OPTION_GONE.

Rules worth knowing

  • Only what is in stock. Every level lists only things you can buy right now. A choice with nothing under it is a 404 whose message points one level up:

    { "success": 0, "errors": [{ "message": "Nothing is in stock for \"discord\" in \"FR\". See /api/v1/catalog/platforms/discord/countries.", "param": "country" }] }
  • Exact ids. Telegram, tg, us and United States are not ids. In a catalogue path they find nothing and answer 404; as a service or country parameter to /request/price or /purchase/sms they are refused with 400. Use telegram and US as the catalogue lists them.

  • Fresh. Prices and stock are refreshed about every two minutes; prices_at says when the prices you are looking at were read.

  • Warming up. Right after a restart the catalogue may answer 503 with Prices are being refreshed. Try again in a few seconds. — retry shortly.

  • Shared numbers aren't in the tree. The tree lists private numbers. Whether a cheaper shared, public-inbox number is on offer for a platform and country is in the shared_inbox field of /request/price.

Caching with ETags

Every level carries an ETag and Cache-Control: private, max-age=30. Send the ETag back as If-None-Match, and while nothing at that level has changed you get a bodiless 304 instead of the list:

curl -i https://numestri.com/api/v1/catalog/platforms/discord/countries
# HTTP/1.1 200 OK
# ETag: W/"…"

curl -i https://numestri.com/api/v1/catalog/platforms/discord/countries -H 'If-None-Match: W/"…"'
# HTTP/1.1 304 Not Modified
  • Tags are weak and compared weakly: W/"x" and "x" match. If-None-Match may hold a comma-separated list, or *.

  • The tag changes whenever the prices behind the level may have changed — including when the prices that apply to you change — so a 304 always means your copy is current.

  • Public prices and your account's prices can have different tags. Keep the tag with the response it came from, and don't reuse an anonymous tag on a keyed request.

  • A request with key= in the URL is never cacheable; send the key in a header if you want caching.

Overview call

GET /api/v1/catalog describes the tree and the three ways to order, so a client can discover it without hard-coding paths. It is public too.

{
  "success": 1,
  "prices_at": "2026-10-14T01:40:00.000Z",
  "platforms_in_stock": 1326,
  "tree": [
    { "level": 1, "list": "platforms", "path": "/api/v1/catalog/platforms" },
    { "level": 2, "list": "countries for a platform", "path": "/api/v1/catalog/platforms/{platform}/countries" },
    { "level": 3, "list": "providers for a platform in a country", "path": "/api/v1/catalog/platforms/{platform}/countries/{country}/providers" },
    { "level": 4, "list": "a provider's options", "path": "/api/v1/catalog/platforms/{platform}/countries/{country}/providers/{provider}/options" }
  ],
  "ordering": {
    "endpoint": "/api/v1/purchase/sms",
    "choose_by": [
      "service + country: the best option, failing over to the next",
      "service + country + provider: that provider only, its best option first",
      "service + country + option: exactly that option at its listed price, or nothing"
    ]
  }
}

Simpler public lists

If you only need names and ids — a dropdown, say — two flat lists keep the shape most verification APIs use. They take no key at all.

Endpoint

Returns

GET /service/retrieve_all[?country=US]

[{ "ID": "telegram", "name": "Telegram", "from_price": "0.99" }]

GET /country/retrieve_all

[{ "ID": "US", "name": "United States", "short_name": "US" }]

ID is the exact id the ordering endpoints take. Both always show public prices.

Updated