Quickstart
Create a key in Dashboard → API keys, fund your balance, then run the three calls below. The order holds the number for 20 minutes while the SMS arrives — poll it, copy the code, finish.
# 1. pick an offer
curl -s "https://smsapi.muneebs.space/v1/catalog/products?country_id=1&service_id=7" \
-H "Authorization: Bearer sk_live_…"
# 2. create the order (idempotent)
curl -s -X POST "https://smsapi.muneebs.space/v1/orders/create" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"product_id": 1042, "max_price": 0.35}'
# 3. poll until otp_code is set, then finish
curl -s "https://smsapi.muneebs.space/v1/orders/ord_9f3x2a" \
-H "Authorization: Bearer sk_live_…"Authentication
Every request carries a bearer key. Live keys read and move real money; sandbox keys exercise the same surface against fake inventory. Keys are shown once at creation and stored hashed — revoke and rotate from the dashboard.
Real numbers and real charges. Rate limited per key.
Same routes, simulated SMS. Never charged, never delivers a real number to a real app.
curl -s "https://smsapi.muneebs.space/v1/balance" \
-H "Authorization: Bearer sk_live_2c9f…a41b"Base URL & envelope
All money fields are integer USD cents — never floats. Every response carries an x-request-id header for support. Success wraps the payload in data; failures wrap a typed error object.
{
"success": true,
"data": {
"public_id": "ord_9f3x2a",
"status": "waiting_sms",
"phone": "+14155550198",
"price_cents": 42,
"otp_code": null,
"expires_at": "2026-10-31T19:04:12Z"
}
}{
"success": false,
"error": {
"code": "NO_NUMBERS",
"message": "No numbers in stock for this offer. Try another operator."
}
}
HTTP/1.1 404 Not Found
x-request-id: req_01j8xk2m9pEndpoints
Cursor pagination on all list routes: pass limit and cursor, read next_cursor back.
/v1/catalog/countriesCountries with dial code and from-price/v1/catalog/services?country_id=Buyable services for a country/v1/catalog/products?country_id=&service_id=Operator-level offers with price and stock/v1/catalog/exchange-rateDisplay-currency conversion rates/v1/orders/createCreate an order: product_id, max_price, metadata/v1/ordersOrder history, cursor paginated/v1/orders/activeOrders currently waiting for an SMS/v1/orders/{id}One order, including otp_code when delivered/v1/orders/cancelCancel a waiting order (refunds)/v1/orders/finishClose a delivered order, stop the timer/v1/orders/resendRequest another SMS on the same number/v1/orders/reactivateRe-open a finished number when supported/v1/balanceWallet balance in integer cents/v1/me/transactionsLedger statement, cursor paginated/v1/webhooksList webhook endpoints/v1/webhooksRegister an endpoint: url, events[]/v1/webhooks/{id}Delete an endpointcurl -s "https://smsapi.muneebs.space/v1/catalog/countries" \
-H "Authorization: Bearer sk_live_…"
curl -s "https://smsapi.muneebs.space/v1/catalog/services?country_id=1" \
-H "Authorization: Bearer sk_live_…"Error codes
Machine-readable codes never change. Branch on the code, not the message text. Retryable statuses are 429, 500, 502 and 503.
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| UNAUTHORIZED | 401 | Missing or revoked API key | Check the Authorization header |
| FORBIDDEN | 403 | Key lacks the required scope | Use a key with the right scopes |
| NOT_FOUND | 404 | Object or route does not exist | Verify the id format |
| NO_NUMBERS | 404 | No stock for that offer | Refresh catalog, pick another product |
| VALIDATION_ERROR | 422 | Body failed schema validation | Fix the field shown in details |
| PRICE_EXCEEDED | 422 | Offer exceeds your max_price | Raise max_price or pick another offer |
| INSUFFICIENT_BALANCE | 402 | Wallet balance too low | Top up and retry |
| IDEMPOTENCY_CONFLICT | 409 | Same key, different payload | Reuse the original payload or a new key |
| RATE_LIMITED | 429 | Token bucket exhausted | Honour the Retry-After header |
| PROVIDER_ERROR | 502 | Upstream supplier failed | Retry with backoff; another operator may work |
| INTERNAL_ERROR | 500 | Unexpected server error | Retry; contact support with x-request-id |
Webhooks
Register an endpoint with POST /v1/webhooks and receive every state change. Deliveries are signed and retried with exponential backoff for eight attempts.
- order.created
- order.sms_received
- order.completed
- order.canceled
- order.expired
X-Signaturesha256=…X-Timestampunix secondsX-Eventevent name
Verify against the raw request body before parsing JSON. The signed string is timestamp + "." + raw_body.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(
rawBody: string,
timestamp: string,
signature: string,
secret: string,
): boolean {
// reject replays older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"sha256=" +
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Rate limits & idempotency
60 requests/minute sustained, burst 30. Catalog reads are cheaper than order writes. 429 responses include Retry-After.
Send an Idempotency-Key on every POST. Replays inside 24h return the original response; a reused key with a different body returns 409.
OpenAPI spec
The full machine-readable contract lives at /openapi.json. Point your generator at it, or import it into Postman/Insomnia and explore.