Skip to content
Developers

API reference

One REST API for catalog, orders and webhooks. Bearer auth, integer-cent money, idempotent POSTs and signed callbacks.

Base URL
https://smsapi.muneebs.space/v1
Auth
Bearer sk_live_…
Envelope
{ success, data }

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_…"
1. create order2. poll order3. finish order

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.

Live
sk_live_2c9f…a41b

Real numbers and real charges. Rate limited per key.

Sandbox
sk_test_8b71…d0e2

Same routes, simulated SMS. Never charged, never delivers a real number to a real app.

Authorization header
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
{
  "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"
  }
}
Failure
{
  "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_01j8xk2m9p

Endpoints

Cursor pagination on all list routes: pass limit and cursor, read next_cursor back.

Catalog
GET/v1/catalog/countriesCountries with dial code and from-price
GET/v1/catalog/services?country_id=Buyable services for a country
GET/v1/catalog/products?country_id=&service_id=Operator-level offers with price and stock
GET/v1/catalog/exchange-rateDisplay-currency conversion rates
Orders
POST/v1/orders/createCreate an order: product_id, max_price, metadata
GET/v1/ordersOrder history, cursor paginated
GET/v1/orders/activeOrders currently waiting for an SMS
GET/v1/orders/{id}One order, including otp_code when delivered
POST/v1/orders/cancelCancel a waiting order (refunds)
POST/v1/orders/finishClose a delivered order, stop the timer
POST/v1/orders/resendRequest another SMS on the same number
POST/v1/orders/reactivateRe-open a finished number when supported
Account
GET/v1/balanceWallet balance in integer cents
GET/v1/me/transactionsLedger statement, cursor paginated
GET/v1/webhooksList webhook endpoints
POST/v1/webhooksRegister an endpoint: url, events[]
DELETE/v1/webhooks/{id}Delete an endpoint
Browse the catalog
curl -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.

CodeHTTPMeaningWhat to do
UNAUTHORIZED401Missing or revoked API keyCheck the Authorization header
FORBIDDEN403Key lacks the required scopeUse a key with the right scopes
NOT_FOUND404Object or route does not existVerify the id format
NO_NUMBERS404No stock for that offerRefresh catalog, pick another product
VALIDATION_ERROR422Body failed schema validationFix the field shown in details
PRICE_EXCEEDED422Offer exceeds your max_priceRaise max_price or pick another offer
INSUFFICIENT_BALANCE402Wallet balance too lowTop up and retry
IDEMPOTENCY_CONFLICT409Same key, different payloadReuse the original payload or a new key
RATE_LIMITED429Token bucket exhaustedHonour the Retry-After header
PROVIDER_ERROR502Upstream supplier failedRetry with backoff; another operator may work
INTERNAL_ERROR500Unexpected server errorRetry; 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.

Events
  • order.created
  • order.sms_received
  • order.completed
  • order.canceled
  • order.expired
Headers
  • X-Signature sha256=…
  • X-Timestamp unix seconds
  • X-Event event name

Verify against the raw request body before parsing JSON. The signed string is timestamp + "." + raw_body.

verify.ts
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

Token bucket per key

60 requests/minute sustained, burst 30. Catalog reads are cheaper than order writes. 429 responses include Retry-After.

Idempotent POSTs

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.

API reference · OTPVAULT