# SafeCode Numbers API

> Rent virtual phone numbers and receive OTPs / SMS programmatically. Pay-as-you-go, single wallet, refunds on failed delivery.

Base URL: `https://api.safecode.vip/v1`

Dashboard: https://safecode.vip/numbers-api
Human docs page: https://safecode.vip/developer-apis/numbers
Full-text (this file): https://safecode.vip/developer-apis/numbers.md
LLM index: https://safecode.vip/developer-apis/numbers/llms.txt

## Highlights

- Simple integration: stable endpoint shapes, predictable statuses, numeric `errno` codes.
- One wallet: API purchases debit your main SafeCode balance. No separate top-up.
- Honest pricing: the price `/request/price` quotes is the price `/purchase/sms` charges.
- Refunds: orders that end without a code (expired, cancelled by you, or withdrawn by the supplier) are refunded in full, automatically.
- Safe retries: send an `Idempotency-Key` header and a retried purchase returns the same order instead of buying twice.

## Authentication

Send your key as a Bearer token. Keys are created at https://safecode.vip/numbers-api and need the `otp:receive` scope.

```
Authorization: Bearer sfcd_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
```

You may also pass the key with the `?key=` query parameter, but prefer the header: URLs end up in logs.

If you set an IP allowlist in the dashboard, requests from any other IP return `errno 1` with HTTP `403`.

## Response envelope

Every response is JSON with the same three fields, plus endpoint-specific data:

```
{ "success": 1, "errno": 0, "message": "success", ...data }
```

Failures set `success: 0` and a non-zero `errno` (table at the bottom).

## Endpoints

All paths below are relative to `https://api.safecode.vip/v1`.

### POST /purchase/sms
Buy a virtual number and start waiting for the SMS.

```
curl -X POST "https://api.safecode.vip/v1/purchase/sms" \
  -H "Authorization: Bearer $SAFECODE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2c1e-9a55-4c0e-8f7a-2b1d6c3e4a90"   -d '{ "country": "us", "service": "telegram" }'
```

Body: `country` (required), `service` (required), `max_price` (optional, USD).

- You are charged the price `/request/price` quotes. `max_price` below that price returns `errno 4` with the current `price`; a higher `max_price` lets the order through when stock is only available at a higher price.
- `Idempotency-Key` (optional header, recommended): any unique string per purchase. A retry with the same key returns the original order (`idempotent_replay: true`). The same key with a different country or service returns `errno 12`; a retry while the first request is still running returns `errno 11`.

`country` and `service` accept a code (`us`, `gb`, `telegram`, `whatsapp`), an exact name, or the UUID returned by `/country/retrieve_all` and `/service/retrieve_all`. Common aliases such as `usa`, `uk` and `uae` are understood. A partial name is accepted only when it matches exactly one entry; otherwise you get `errno 2`, so the API never guesses the wrong country.

Response:
```
{
  "success": 1,
  "errno": 0,
  "message": "Number ordered successfully.",
  "order_id": "e0b1…",
  "phonenumber": "+14155551234",
  "number": "+14155551234",
  "country": "United States",
  "country_code": "US",
  "service": "Telegram",
  "cost": 0.32,
  "currency": "USD",
  "expires_in": 1200,
  "expires_at": "2026-07-01T15:04:05Z",
  "cancel_available_at": "2026-07-01T14:47:05Z",
  "balance": 12.83
}
```

A number stays reserved for `expires_in` seconds (20 minutes). Poll `/sms/check` until a code arrives or the order expires.

### POST /sms/check
Poll for the SMS. `status` values are:
- `1` waiting
- `3` received; `code` and `sms` are populated
- `4` expired (refunded)
- `6` cancelled by you or withdrawn by the supplier (refunded)

```
curl -X POST "https://api.safecode.vip/v1/sms/check" \
  -H "Authorization: Bearer $SAFECODE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "orderid": "e0b1…" }'
```

Response while waiting:
```
{ "success": 1, "errno": 0, "status": 1, "order_id": "e0b1…", "phonenumber": "+14155551234", "expires_at": "…", "sms": null, "code": null, "message": "Waiting for SMS" }
```

Response once received:
```
{ "success": 1, "errno": 0, "status": 3, "code": "483920", "sms": "Your Telegram code: 483920", "full_sms": "Your Telegram code: 483920", "phonenumber": "+14155551234", "order_id": "e0b1…" }
```

Received codes are cached, so repeated checks on a completed order are instant and never re-poll the upstream carrier.

### POST /sms/cancel
Cancel a number and refund your wallet.

- Orders can be cancelled 3 minutes after purchase (`cancel_available_at`), because most codes arrive in that window. Earlier returns `errno 10` with `wait_seconds`.
- If the SMS arrived first, nothing is cancelled: you get `errno 6` with `status_code: 3` and the `code`, so you never lose a code you paid for.
- A finished order returns `errno 6` with `status` (text) and `status_code` (1, 3, 4 or 6).

```
curl -X POST "https://api.safecode.vip/v1/sms/cancel" \
  -H "Authorization: Bearer $SAFECODE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "orderid": "e0b1…" }'
```

Response:
```
{ "success": 1, "errno": 0, "status": 6, "order_id": "e0b1…", "refunded": 0.32, "balance": 12.83, "message": "Order cancelled and refunded." }
```

### POST /sms/resend
Ask the destination service to resend its OTP. Not available for every service; see the WhatsApp note below. Unsupported orders return `errno 8`; keep polling `/sms/check` instead.

```
curl -X POST "https://api.safecode.vip/v1/sms/resend" \
  -H "Authorization: Bearer $SAFECODE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "orderid": "e0b1…" }'
```

### GET /service/retrieve_all
Every service currently purchasable through the API (a few hundred), sorted by name.

```
{ "success": 1, "errno": 0, "services": [ { "id": "…", "name": "Telegram", "code": "telegram" }, … ] }
```

### GET /country/retrieve_all
Every country currently purchasable through the API.

```
{ "success": 1, "errno": 0, "countries": [ { "id": "…", "name": "United States", "code": "US" }, … ] }
```

### GET /request/price?country=us&service=telegram
Quote the current price before purchasing. This is the exact amount `/purchase/sms` will debit.

```
{ "success": 1, "errno": 0, "price": 0.32, "currency": "USD", "country": "United States", "service": "Telegram" }
```

### GET /request/balance
Current wallet balance in USD.

```
{ "success": 1, "errno": 0, "balance": 12.83, "currency": "USD" }
```

### GET /request/history?limit=25&offset=0&status=3
API-purchased orders, newest first, with `total` for paging.

- `limit` 1 to 100 (default 25), `offset` for paging, `status` to filter by status code (1, 3, 4 or 6).
- Each order has `status_code` (same values as `/sms/check`) and `service`, `service_code`, `country`, `country_code`.

## WhatsApp

WhatsApp numbers are available and are fulfilled through a dedicated upstream pool rather than the default one. Two things differ:

- `/sms/resend` is not supported for WhatsApp orders and returns `errno 8`. Keep polling `/sms/check`; the activation window is the usual 20 minutes.
- Availability is per country. Ask `/request/price?country=…&service=whatsapp` first; `errno 4` means that country has no WhatsApp stock right now.

## Webhooks

Add an endpoint in the dashboard (Webhooks tab) and we POST each order's outcome to it instead of you polling `/sms/check`.

Events:
- `number.received` (status 3): the SMS arrived; `data.code` holds the code.
- `number.cancelled` (status 6): you cancelled, or the supplier withdrew the number; refunded.
- `number.expired` (status 4): no SMS before expiry; refunded.

Body:
```
{
  "event": "number.received",
  "ts": 1790700000,
  "data": {
    "order_id": "b2566cc8-…", "livemode": true, "status": "received", "status_code": 3,
    "phone_number": "447544323221", "code": "482913",
    "service": "Facebook", "service_code": "facebook", "country": "United Kingdom", "country_code": "GB",
    "price_usd": 0.37, "currency": "USD", "refunded_usd": null,
    "created_at": "…", "finished_at": "…"
  }
}
```

Verify every request: header `X-SafeCode-Signature: t=<unix>,v1=<hex>` is HMAC-SHA256 of `"<t>.<raw body>"` with your endpoint's signing secret (shown once when you add it). Reject if it doesn't match or `t` is more than 5 minutes old. Reply 2xx within 10 seconds; failures are retried 5 more times, after 1, 5, 30, 120 and 720 minutes (about 15 hours). If an event uses up every retry while the endpoint keeps failing, the endpoint is turned off; turn it back on in the dashboard. Events can repeat, so dedupe on `order_id` + `event`.

## Sandbox

Create a Sandbox key in the dashboard (`sfcd_test_…`). Every endpoint works the same, with fake numbers and no charges (balance always 100). Code `123456` arrives about 10 seconds after purchase, the 3-minute cancel rule applies, and webhooks fire with `livemode: false`. Purchase, check, cancel, balance and history responses include `livemode: false`; catalog and price endpoints are shared with live. Sandbox keys work with the Numbers API only (the Messaging API rejects them).

Pick an outcome with `sandbox_outcome` on `/purchase/sms`:

| sandbox_outcome | result |
|-----------------|--------|
| `code` (default) | code 123456 after ~10 s |
| `slow_code` | code after ~90 s |
| `no_code` | no code for 20 min (test cancelling, allowed after 3 min) |
| `expire` | no code, expires after ~45 s |
| `no_stock` | purchase fails, errno 4 |
| `no_balance` | purchase fails, errno 3 |

## Rate limits

Each key has a token bucket (your plan's requests per second), and purchases have a separate, tighter bucket per account. When one is empty you receive `errno 7` with HTTP `429`; back off briefly and retry. Poll `/sms/check` every 3 to 5 seconds per order.

## Error codes (`errno`)

| errno | HTTP | meaning |
|-------|------|---------|
| 0  | 200 | success |
| 1  | 401 | API key missing or invalid; `403` when the key lacks `otp:receive`, the account is inactive, or the IP is not on your allowlist |
| 2  | 400 | invalid or missing parameters |
| 3  | 400 | insufficient balance; response includes `required` and `balance` |
| 4  | 400 | no numbers available for this service / country (also returned when a pair has no price yet) |
| 5  | 404 | order not found, or it belongs to another key |
| 6  | 400 | order is already finished; on a cancel, `status_code: 3` with `code` means the SMS arrived first |
| 7  | 429 | rate limit exceeded |
| 8  | 400 | number service error; the upstream carrier declined or failed |
| 9  | 503 | this service is currently disabled |
| 10 | 400 | too early to cancel; `wait_seconds` says how long is left |
| 11 | 409 | a purchase with this `Idempotency-Key` is still running |
| 12 | 409 | this `Idempotency-Key` was already used for a different purchase |
| 99 | 500 | internal server error |

Failures also set `success: 0`.

## Getting started

1. Create a SafeCode account and generate a key at https://safecode.vip/numbers-api.
2. Fund your wallet.
3. `GET /request/price` to confirm the pair is in stock and see the cost.
4. `POST /purchase/sms` → then poll `POST /sms/check` every few seconds until `status` is `3`.
5. If nothing arrives you can `POST /sms/cancel` from 3 minutes after purchase, or just let the order expire: both are refunded automatically.
