API Documentation

Buy virtual numbers and read verification codes from your own code

Get your API keyOpen the playgroundhttps://api.socialcheap.net/v1

You read the catalog, buy a number, then poll for the code. Three calls in the happy path, and no list-orders endpoint: store the id the create call returns.

Copy the ids exactly as you receive them. Country, service and carrier values are opaque. One can be a string (whatsapp) or a number (1012), they are not shared between catalogs, and they change or disappear as stock moves. Take each one from the catalog response and pass it straight back. Never guess one, and never reuse a value you cached earlier. Re-read the catalog before you order.

Quick start

Send your key in the X-API-Key header on every request.

BASE=https://api.socialcheap.net/v1
KEY=SCKEY_your_api_key
 
# Every id below is an example. Read each one from the catalog and copy it exactly.
PROVIDER=global            # GET /sim/providers
COUNTRY=afghanistan        # GET /sim/providers/$PROVIDER/countries
SERVICE=linkedin           # GET /sim/providers/$PROVIDER/countries/$COUNTRY/services
CARRIER=virtual4           # GET /sim/providers/$PROVIDER/countries/$COUNTRY/services/$SERVICE/pricing
 
# Price the service, then buy with the carrier you were just quoted
curl -s "$BASE/sim/providers/$PROVIDER/countries/$COUNTRY/services/$SERVICE/pricing" \
  -H "X-API-Key: $KEY"
 
curl -s -X POST "$BASE/sim/orders" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"provider\":\"$PROVIDER\",\"country\":\"$COUNTRY\",\"service\":\"$SERVICE\",\"carrier\":\"$CARRIER\"}"
 
# Read the code, using the id the create call returned
curl -s "$BASE/sim/orders/SCG-NLQXAZLSEW" \
  -H "X-API-Key: $KEY"

Store the id from the create response; it is how you read the order afterwards. Poll every 3 to 5 seconds while status is active, and stop once code is set.

Authentication

Keys are issued per account from the dashboard. One key is live at a time: issuing a new one revokes the previous key immediately.

Headers
X-API-Key: SCKEY_your_api_key
Accept: application/json
ResponseMeaning
401Key missing, unknown, or revoked
403Account suspended

Authenticated account

GET /v1/me returns the account the key belongs to.

200 OK
{
  "msg": "Authenticated",
  "data": {
    "id": "USR-8f14e45f",
    "username": "ada",
    "email": "ada@example.com",
    "balance": 15250.0
  }
}
FieldTypeNotes
idstringYour account id, a stable handle for your own records
usernamestringDisplay only
emailstringDisplay only
balancenumberNaira, major units, directly comparable to price and cost

No path parameter and no account id: the key identifies the account. As a GET, it spends read budget.

Responses

Success and failure share one envelope.

200 OK
{
  "msg": "Countries",
  "data": [{ "code": "usa", "name": "USA", "iso": "us" }]
}

Every answer is msg plus exactly one payload key. The key is the signal: a failure never arrives as "data": null:

StatusKeyValue
2xxdataThe payload: a list, or an object
4xx/5xxerrorsA field map (422), or [] when nothing is field-specific
401 Unauthorized
{
  "msg": "Invalid API key",
  "errors": []
}

Action endpoints return "data": [] on success. Branch on the HTTP status; msg is a human-readable label.

StatusMeaning
200Success
201Order created
400Cannot be completed: balance, availability, or the action was refused
401Key missing, invalid, or revoked
403Account suspended
404Unknown order, or an order on another account
422Body or query failed validation
429Rate limited
500Server error; retry with backoff

A 400 is not always a complaint about your request. The catalog can refuse the action (This catalog does not support ban), or the number can go out of stock between pricing and purchase. Re-read pricing and try again later.

Validation errors (422)

Field-keyed, so each message pins to the input it came from:

422 Unprocessable entity
{
  "msg": "Unprocessable entity",
  "errors": {
    "carrier": ["Field required"]
  }
}
  • errors maps a field to one or more messages, and the key is the field name.
  • A problem that belongs to the whole body is keyed body; a nested field uses a dotted path (params.count).
  • More than one field can fail at once. Ordering premium without a country and a carrier names both.

Prices and balances are naira in major units: 1500.0 is ₦1,500, directly comparable to the balance from /me.

Rate limits

Limits apply per key and per IP address, and the lower of the two applies.

ScopeRead (GET, HEAD)Write (everything else)
Per API key100 / minute20 / minute
Per IP address100 / minute20 / minute

Services that call from the same address share that IP budget. A heavy integration should arrive from its own egress IP.

HeaderMeaning
X-RateLimit-LimitAllowance for the bucket
X-RateLimit-RemainingRequests left in the window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait

These are returned with every limited response, including 429. On a 429, wait out Retry-After and back off exponentially if it repeats.

Catalog

The catalog is a tree: catalog, country, service, then carrier pricing. One tree per provider, and the ids are not shared between them: usa is global's, 1 is ultra's.

Values are opaque: a string or a number, and subject to change. Read each one, use it, then read it again before you order.

Every service list is country-scoped. There is no catalog-wide one. Walk a country at a time and pick from the services it has stock for.

Endpoints
GET /sim/providers
GET /sim/providers/{provider}/countries
GET /sim/providers/{provider}/countries/{country}/services
GET /sim/providers/{provider}/countries/{country}/services/{service_id}/pricing
GET /sim/providers/{provider}/areacodes
GET/sim/providers

The catalogs you can order from. id is what you pass as provider.

200 OK
{
  "msg": "Providers",
  "data": [
    {
      "id": "global",
      "name": "Global",
      "actions": ["cancel", "ban", "finish"]
    },
    { "id": "premium", "name": "Premium", "actions": [] },
    { "id": "ultra", "name": "Ultra", "actions": ["cancel"] }
  ]
}
FieldTypeNotes
idstringPass as provider
namestringDisplay only
actionsstring[]Order actions this catalog supports. Check it before you offer cancel, ban or finish in a UI: premium supports none, and an unsupported action answers 400
GET/sim/providers/{provider}/countries

Codes are per catalog: the same country is usa in global and 1 in ultra, and its name differs too. Never carry a code across providers; take it from this list each time. Sending one catalog's code to another is refused with a 400.

200 OK
{
  "msg": "Countries",
  "data": [{ "code": "usa", "name": "USA", "iso": "us" }]
}
FieldTypeNotes
codestring or numberPass as country, exactly as returned
namestringDisplay only
isostring or nullISO 3166 alpha-2, for display or mapping
GET/sim/providers/{provider}/countries/{country}/services

Services with stock in that country.

200 OK
{
  "msg": "Services",
  "data": [{ "code": "whatsapp", "name": "WhatsApp" }]
}
FieldTypeNotes
codestring or numberPass as service, exactly as returned
namestringDisplay only
GET/sim/providers/{provider}/countries/{country}/services/{service_id}/pricing

The carriers that can serve this service, and what each costs. Read it immediately before ordering; it is the last step of the walk.

Out-of-stock carriers are left out, so this list is the set you can order from.

premium serves a single country and a single carrier, so it answers one row.

200 OK
{
  "msg": "Pricing",
  "data": [{ "carrier": "virtual4", "name": "virtual4", "price": 459.85 }]
}
FieldTypeNotes
carrierstringPass as carrier. An opaque id, never a network name. Send the one from the quoted row
namestringDisplay only. The readable label for that row: virtual4, Foxtrot
pricenumberNaira. What the order costs
GET/sim/providers/{provider}/areacodes

Area codes, for services that require one. premium is the only catalog that offers them; every other answers [].

200 OK
{
  "msg": "Area codes",
  "data": [{ "code": "212", "region": "New York" }]
}
FieldTypeNotes
codestringPass in area_codes
regionstring or nullDisplay only

Orders

POST/sim/orders

Creates an order and returns 201 with the order object.

Request body
{
  "provider": "global",
  "country": "afghanistan",
  "service": "linkedin",
  "carrier": "virtual4"
}
FieldRequiredNotes
provideryesA catalog id
countryyesThe code from the countries list
serviceyesA service code
carrieryesFrom the pricing row you quoted
area_codesnoOnly for services that require one
capabilitynopremium only, see below

capability is read by the premium catalog and ignored by global and ultra, which accept the field and do nothing with it. Three values are valid; the default is sms.

ValueMeaning
smsDefault when omitted
voiceVoice verification
smsAndVoiceComboBoth

It changes the premium price, and the pricing endpoint cannot express it, so the cost in this response is the quote. Send one of the three values, or omit the field.

There is one body for every catalog: country and carrier are always required, so a client needs a single code path. global and ultra price per carrier; premium serves one country (usa) and one carrier row, and takes the value from that row. A missing field returns 422 naming it.

cost in the create response is the amount charged.

201 Created
{
  "msg": "Order created",
  "data": {
    "id": "SCG-NLQXAZLSEW",
    "number": "+93788503317",
    "service": "linkedin",
    "country": "afghanistan",
    "status": "active",
    "code": null,
    "message": null,
    "expires_at": "2026-10-15T09:38:53.484428Z",
    "cost": 459.85,
    "provider": "global"
  }
}
ResponseMeaning
400Balance too low, the number is no longer available, or the request was refused. Re-read pricing and retry
503Purchases are paused for that catalog; retry shortly
GET/sim/orders/{order_id}

Reads one order. Once the SMS arrives, code and message are filled in.

Code received
{
  "msg": "Order",
  "data": {
    "id": "SCG-NLQXAZLSEW",
    "number": "+93788503317",
    "service": "linkedin",
    "country": "afghanistan",
    "status": "active",
    "code": "428462",
    "message": "[LinkedIn] 428462 is your verification code, valid for 5 minutes.",
    "expires_at": "2026-10-15T09:38:53.484428Z",
    "cost": 459.85,
    "provider": "global"
  }
}
FieldTypeNotes
idstringYour reference for the order (SCG-, SCP- or SCU- by catalog). Store it; there is no list endpoint
numberstring or nullThe assigned number, in E.164 form
servicestring or nullThe service you sent
countrystring or nullThe country for this order, lowercased. Can be null
statusstringactive, completed, banned, cancelled, or expired
codestring or nullThe most recent code. Null until one arrives
messagestring or nullThe full SMS text. Some messages carry the code only in the body
expires_atstring or nullRFC 3339. UTC is written Z, with fractional seconds only when they are non-zero
costnumberNaira charged
providerstringCatalog id

Only the most recent code is returned. If the number receives a second SMS, poll again to read it.

StatusMeaning
activeLive, keep polling
completedFinished by you
bannedBanned by you
cancelledCancelled before the code arrived
expiredValidity window elapsed. The charge is returned

Only active is worth polling; every other status is final.

An expired order is refunded automatically.

POST/sim/orders/{id}/finish

Close the order when you are done with the number. Returns data: []. finish does not refund.

It marks the order completed, so it is only available once a code has arrived. A fresh order answers 400.

Also available
POST /sim/orders/{id}/cancel   cancel and, where eligible, refund
POST /sim/orders/{id}/ban      block the number from reuse

cancel and ban refund to your wallet while the number has not delivered a code and the window has not elapsed, so cancel early rather than after the SMS arrives.

actions from /sim/providers says what a catalog supports, not what a given order will accept. An eligible action can still be refused, so offer it and handle the 400.