API Documentation

Programmatic access to virtual numbers, orders, and account balance.

Overview

All money fields on the /v1 API are in IDR (Indonesian Rupiah), as whole integer units — for example "price": 15000 and "balance": 500000 mean Rp 15,000 and Rp 500,000. For a USD-native projection of the same ledger, switch to the v2 API using the version toggle above.

Authentication

All API requests require a Bearer token. Generate one from Account Settings in the dashboard, then include it in every request:

Authorization: Bearer YOUR_API_TOKEN

Requests without a valid token receive a 401 UNAUTHORIZED response.

Base URL

All endpoint paths below are relative to:

https://api.smscode.gg/v1

Response Format

Every response returns JSON with a consistent envelope. All responses include an x-request-id header for debugging.

Success
{
  "success": true,
  "data": { ... }
}
Error
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message"
  }
}

All money fields on the /v1 API are in IDR (Indonesian Rupiah), as whole integer units — for example "price": 15000 and "balance": 500000 mean Rp 15,000 and Rp 500,000. For a USD-native projection of the same ledger, switch to the v2 API using the version toggle above.

GET /catalog/countries

Returns a list of all available countries.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v1/catalog/countries \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/countries", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/countries",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 6,
      "code": "ID",
      "name": "Indonesia",
      "dial_code": "+62",
      "emoji": "🇮🇩",
      "active": true
    }
  ]
}
GET /catalog/services

Returns a list of available services (platforms). Optionally filter by country.

Query Parameters

NameTypeRequiredDescription
country_idintegerNoFilter services available for this country

Example Request

curl -s "https://api.smscode.gg/v1/catalog/services?country_id=6" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/services?country_id=6", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/services",
    params={"country_id": 6},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 3,
      "code": "wa",
      "name": "WhatsApp",
      "active": true
    }
  ]
}
GET /catalog/products

Returns a paginated list of available products. Filter by country and/or platform.

Query Parameters

NameTypeRequiredDescription
country_idintegerNoFilter by country ID
platform_idintegerNoFilter by platform/service ID
sortstringNoSort order: price_asc (default), price_desc, available_asc, available_desc, name_asc, name_desc
limitintegerNoResults per page (1-10,000, default 1,000)
pageintegerNoPage number (min 1, default 1)

Example Request

curl -s "https://api.smscode.gg/v1/catalog/products?country_id=6&platform_id=3&limit=10&page=1" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  country_id: "6", platform_id: "3", limit: "10", page: "1",
});
const res = await fetch(`https://api.smscode.gg/v1/catalog/products?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/products",
    params={"country_id": 6, "platform_id": 3, "limit": 10, "page": 1},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "available": 42,
      "price": 15000,
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1 }
}
GET /catalog/exchange-rate

Returns the current USD/IDR exchange rate used for currency conversion.

Query Parameters

NameTypeRequiredDescription
pairstringNoCurrency pair (default: USD/IDR)

Example Request

curl -s "https://api.smscode.gg/v1/catalog/exchange-rate?pair=USD/IDR" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/exchange-rate?pair=USD/IDR", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/exchange-rate",
    params={"pair": "USD/IDR"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "pair": "USD/IDR",
    "base_currency": "USD",
    "quote_currency": "IDR",
    "rate": 16250
  }
}
GET /balance

Returns the authenticated user's account balance.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v1/balance \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/balance", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/balance",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 500000
  }
}
GET /orders

Returns a list of the authenticated user's orders, sorted by most recent. Supports filtering by status and pagination via offset.

Query Parameters

NameTypeRequiredDescription
limitintegerNoMax results (1-100, default 20)
offsetintegerNoNumber of results to skip (default 0)
statusstringNoFilter by status: ACTIVE, OTP_RECEIVED, COMPLETED, CANCELED, EXPIRED (case-insensitive)

Example Request

curl -s "https://api.smscode.gg/v1/orders?limit=5&status=ACTIVE" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  limit: "5", status: "ACTIVE", offset: "0",
});
const res = await fetch(`https://api.smscode.gg/v1/orders?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders",
    params={"limit": 5, "status": "ACTIVE", "offset": 0},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "ACTIVE",
      "created_at": "2026-02-25T10:00:00+00:00",
      "product_id": 142,
      "phone_number": "+6281234567890",
      "amount": 15000,
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:20:00+00:00",
      "canceled_at": null,
      "failed_reason": null
    }
  ]
}
GET /orders/{id}

Returns a single order by ID. Only returns orders owned by the authenticated user.

Path Parameters

NameTypeRequiredDescription
idintegerYesOrder ID (path parameter)

Example Request

curl -s https://api.smscode.gg/v1/orders/1001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/orders/1001", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders/1001",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "id": 1001,
    "status": "OTP_RECEIVED",
    "created_at": "2026-02-25T10:00:00+00:00",
    "product_id": 142,
    "phone_number": "+6281234567890",
    "amount": 15000,
    "otp_code": "123456",
    "otp_received_at": "2026-02-25T10:05:00+00:00",
    "expires_at": "2026-02-25T10:20:00+00:00",
    "canceled_at": null,
    "failed_reason": null
  }
}
GET /orders/active

List all currently active orders (ACTIVE + OTP_RECEIVED). Use this to poll for OTP status updates.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v1/orders/active \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/orders/active", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders/active",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "OTP_RECEIVED",
      "otp_code": "123456",
      "otp_message": "Your verification code is 123456",
      "otp_received_at": "2026-02-25T10:05:00+00:00",
      "expires_at": "2026-02-25T10:20:00+00:00",
      "failed_reason": null
    },
    {
      "id": 1002,
      "status": "ACTIVE",
      "otp_code": null,
      "otp_message": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:50:00+00:00",
      "failed_reason": null
    }
  ]
}
POST /orders/create

Creates a new virtual number order. Deducts balance automatically. Supports an optional Idempotency-Key header to prevent duplicate orders on network retries.

Request Body

NameTypeRequiredDescription
product_idintegerYesID of the product to order
quantityintegerNoNumber of items (1-100, default 1)

Pass an Idempotency-Key header (any unique string) to safely retry requests without creating duplicate orders.

Example Request

curl -s -X POST https://api.smscode.gg/v1/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-123" \
  -d '{"product_id":142,"quantity":1}'
const res = await fetch("https://api.smscode.gg/v1/orders/create", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Idempotency-Key": "unique-request-id-123",
  },
  body: JSON.stringify({ product_id: 142, quantity: 1 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/create",
    json={"product_id": 142, "quantity": 1},
    headers={
        "Authorization": "Bearer YOUR_API_TOKEN",
        "Idempotency-Key": "unique-request-id-123",
    })
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": 1002,
        "status": "ACTIVE",
        "phone_number": "+6281234567891",
        "otp_code": null,
        "otp_received_at": null,
        "expires_at": "2026-02-25T10:50:00+00:00",
        "failed_reason": null
      }
    ],
    "failed_count": 0
  }
}
POST /orders/cancel

Cancel an active order. The rental cost is refunded to your account balance.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to cancel

Example Request

curl -s -X POST https://api.smscode.gg/v1/orders/cancel \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/cancel", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/cancel",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": 15000,
    "new_balance": 515000
  }
}
POST /orders/finish

Mark an order as completed after receiving the OTP. This releases the number immediately instead of waiting for expiry.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to finish

Example Request

curl -s -X POST https://api.smscode.gg/v1/orders/finish \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/finish", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/finish",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "COMPLETED"
  }
}
POST /orders/resend

Request the platform to resend the SMS to the rented number. Not all platforms support resending — check the resent field in the response.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to resend SMS for

Example Request

curl -s -X POST https://api.smscode.gg/v1/orders/resend \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/resend", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/resend",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "ACTIVE",
    "resent": true
  }
}
GET /webhook

Returns your current webhook notification configuration.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v1/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/webhook", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/webhook",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}
PATCH /webhook

Update your webhook URL and/or secret. A secret is auto-generated when you set a URL for the first time. Send an empty string to clear. URL must use HTTPS.

Request Body

NameTypeRequiredDescription
webhook_urlstringNoHTTPS URL to receive webhook events (empty string to clear)
webhook_secretstringNoShared secret for HMAC-SHA256 signature (auto-generated if omitted on first set)

At least one field is required.

Example Request

curl -s -X PATCH https://api.smscode.gg/v1/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com/webhook"}'
const res = await fetch("https://api.smscode.gg/v1/webhook", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhook_url: "https://example.com/webhook" }),
});
const data = await res.json();
import requests

res = requests.patch("https://api.smscode.gg/v1/webhook",
    json={"webhook_url": "https://example.com/webhook"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}
POST /webhook/test

Send a test event to your configured webhook URL. Returns the HTTP status code from your server. Useful for verifying your endpoint is working before going live.

Parameters

None

Example Request

curl -s -X POST https://api.smscode.gg/v1/webhook/test \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/webhook/test", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/webhook/test",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "status_code": 200
  }
}

Webhook Notifications

Configure a webhook URL to receive real-time push notifications for order events instead of polling. This is the recommended approach for bot scripts.

Events

EventTrigger
order.otp_receivedOTP code delivered to the rented number
order.completedOrder marked as completed (manually or by expiry)
order.expiredOrder expired without OTP (balance refunded)
order.canceledOrder canceled by user (balance refunded)

Payload

Webhook POST Body
{
  "event": "order.otp_received",
  "timestamp": "2026-02-25T12:00:00+00:00",
  "data": {
    "order_id": 1001,
    "phone_number": "+628123456789",
    "otp_code": "1234",
    "otp_message": "Your verification code is 1234",
    "product_id": 42,
    "country": "Indonesia",
    "platform": "WhatsApp"
  }
}

Signature Verification

Each webhook request includes an X-Webhook-Signature header with an HMAC-SHA256 signature of the request body, using your webhook_secret as the key:

X-Webhook-Signature: sha256={HMAC-SHA256(body, webhook_secret)}

Verify this signature on your server to ensure the request is authentic. Delivery is fire-and-forget with a 3-second timeout and no retries.

Rate Limits

API requests are rate-limited per endpoint group. Exceeding the limit returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.

Endpoint GroupLimitWindow
Catalog (countries, services, products, exchange-rate)5,000 requests60 seconds
Balance600 requests60 seconds
Order reads (list, get, active)5,000 requests60 seconds
Order create3,000 requests60 seconds
Order cancel1,000 requests60 seconds
Order actions (finish, resend)1,000 requests60 seconds
Webhook config (get, update)600 requests60 seconds
Webhook test10 requests60 seconds

Error Codes

Error responses include one of these codes in error.code:

CodeHTTPDescription
UNAUTHORIZED401Missing or invalid API token
FORBIDDEN403Access denied
NOT_FOUND404Resource not found (order, exchange rate, etc.)
CONFLICT409Duplicate request or resource conflict
INSUFFICIENT_BALANCE409Not enough balance to create order
VALIDATION_ERROR422Request parameters failed validation
RATE_LIMIT_EXCEEDED429Too many requests (check Retry-After header)
INTERNAL_ERROR500Internal server error
PROVIDER_ERROR422Upstream SMS provider rejected the request
CANCEL_TOO_EARLY409Order too recent to cancel — wait 2 minutes
SERVICE_UNAVAILABLE503Service temporarily unavailable (maintenance)

Overview

All money fields on the /v2 API are USD, returned as a money object — { "amount": "0.92", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" }. amount is a decimal string; canonical_amount is the exact IDR ledger value (use it for reconciliation). The USD/IDR rate applied is disclosed once per response in meta.fx. v2 is a render-time USD projection over the same IDR ledger as v1 — it never stores or transacts USD.

Migrating v1 → v2

Authentication

All API requests require a Bearer token. Generate one from Account Settings in the dashboard, then include it in every request:

Authorization: Bearer YOUR_API_TOKEN

Requests without a valid token receive a 401 UNAUTHORIZED response.

Base URL

All endpoint paths below are relative to:

https://api.smscode.gg/v2

Response Format

Every response returns JSON with a consistent envelope. All responses include an x-request-id header for debugging.

Success
{
  "success": true,
  "data": { ... }
}
Error
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message"
  }
}

v2 error: no usable exchange rate (503)

503 Service Unavailable · Retry-After: 60
{
  "success": false,
  "error": { "code": "FX_RATE_UNAVAILABLE", "message": "USD/IDR exchange rate is unavailable" }
}

All money fields on the /v2 API are USD, returned as a money object — { "amount": "0.92", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" }. amount is a decimal string; canonical_amount is the exact IDR ledger value (use it for reconciliation). The USD/IDR rate applied is disclosed once per response in meta.fx. v2 is a render-time USD projection over the same IDR ledger as v1 — it never stores or transacts USD.

GET /catalog/countries

Returns a list of all available countries.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v2/catalog/countries \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/countries", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/countries",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 6,
      "code": "ID",
      "name": "Indonesia",
      "dial_code": "+62",
      "emoji": "🇮🇩",
      "active": true
    }
  ]
}

Identical to v1 — only the base path changes (/v1/v2).

GET /catalog/services

Returns a list of available services (platforms). Optionally filter by country.

Query Parameters

NameTypeRequiredDescription
country_idintegerNoFilter services available for this country

Example Request

curl -s "https://api.smscode.gg/v2/catalog/services?country_id=6" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/services?country_id=6", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/services",
    params={"country_id": 6},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 3,
      "code": "wa",
      "name": "WhatsApp",
      "active": true
    }
  ]
}

Identical to v1 — only the base path changes (/v1/v2).

GET /catalog/products

Returns a paginated list of available products. Filter by country and/or platform.

Query Parameters

NameTypeRequiredDescription
country_idintegerNoFilter by country ID
platform_idintegerNoFilter by platform/service ID
sortstringNoSort order: price_asc (default), price_desc, available_asc, available_desc, name_asc, name_desc
limitintegerNoResults per page (1-10,000, default 1,000)
pageintegerNoPage number (min 1, default 1)

Example Request

curl -s "https://api.smscode.gg/v2/catalog/products?country_id=6&platform_id=3&limit=10&page=1" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  country_id: "6", platform_id: "3", limit: "10", page: "1",
});
const res = await fetch(`https://api.smscode.gg/v2/catalog/products?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/products",
    params={"country_id": 6, "platform_id": 3, "limit": 10, "page": 1},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "available": 42,
      "price": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1, "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2: money fields are USD money objects and the response carries a single meta.fx { pair, rate, rate_as_of }. rate is whole IDR per 1 USD, so USD = canonical_amount / rate. Totals use 2 decimals; per-item prices/refunds use 4. A strictly-positive amount never rounds to 0.00. rate_as_of is the rate's RFC3339 timestamp (+00:00 form) or null when no timestamp is recorded.

v2 only: if no usable USD/IDR rate exists, money endpoints return 503 FX_RATE_UNAVAILABLE with a Retry-After header instead of a money body. v1 never returns this.

GET /catalog/exchange-rate

Returns the current USD/IDR exchange rate used for currency conversion.

Parameters

None — v2 always returns USD/IDR; the v1 ?pair parameter is ignored.

Example Request

curl -s "https://api.smscode.gg/v2/catalog/exchange-rate?pair=USD/IDR" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/exchange-rate?pair=USD/IDR", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/exchange-rate",
    params={"pair": "USD/IDR"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" }
}

v2: returns { pair, rate, rate_as_of } (no base_currency/quote_currency, no meta wrapper — the rate is the data). ?pair is ignored — v2 always returns USD/IDR (v1 honors ?pair). Returns 503 FX_RATE_UNAVAILABLE if no usable rate exists.

GET /balance

Returns the authenticated user's account balance.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v2/balance \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/balance", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/balance",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "balance": { "amount": "30.77", "currency": "USD", "canonical_amount": 500000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2: money fields are USD money objects and the response carries a single meta.fx { pair, rate, rate_as_of }. rate is whole IDR per 1 USD, so USD = canonical_amount / rate. Totals use 2 decimals; per-item prices/refunds use 4. A strictly-positive amount never rounds to 0.00. rate_as_of is the rate's RFC3339 timestamp (+00:00 form) or null when no timestamp is recorded.

v2 only: if no usable USD/IDR rate exists, money endpoints return 503 FX_RATE_UNAVAILABLE with a Retry-After header instead of a money body. v1 never returns this.

GET /orders

Returns a list of the authenticated user's orders, sorted by most recent. Supports filtering by status and pagination via offset.

Query Parameters

NameTypeRequiredDescription
limitintegerNoMax results (1-100, default 20)
offsetintegerNoNumber of results to skip (default 0)
statusstringNoFilter by status: ACTIVE, OTP_RECEIVED, COMPLETED, CANCELED, EXPIRED (case-insensitive)

Example Request

curl -s "https://api.smscode.gg/v2/orders?limit=5&status=ACTIVE" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  limit: "5", status: "ACTIVE", offset: "0",
});
const res = await fetch(`https://api.smscode.gg/v2/orders?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders",
    params={"limit": 5, "status": "ACTIVE", "offset": 0},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "ACTIVE",
      "created_at": "2026-02-25T10:00:00+00:00",
      "product_id": 142,
      "phone_number": "+6281234567890",
      "amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:20:00+00:00",
      "canceled_at": null,
      "failed_reason": null
    }
  ],
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2: money fields are USD money objects and the response carries a single meta.fx { pair, rate, rate_as_of }. rate is whole IDR per 1 USD, so USD = canonical_amount / rate. Totals use 2 decimals; per-item prices/refunds use 4. A strictly-positive amount never rounds to 0.00. rate_as_of is the rate's RFC3339 timestamp (+00:00 form) or null when no timestamp is recorded.

v2 only: if no usable USD/IDR rate exists, money endpoints return 503 FX_RATE_UNAVAILABLE with a Retry-After header instead of a money body. v1 never returns this.

GET /orders/{id}

Returns a single order by ID. Only returns orders owned by the authenticated user.

Path Parameters

NameTypeRequiredDescription
idintegerYesOrder ID (path parameter)

Example Request

curl -s https://api.smscode.gg/v2/orders/1001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/orders/1001", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders/1001",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "id": 1001,
    "status": "OTP_RECEIVED",
    "created_at": "2026-02-25T10:00:00+00:00",
    "product_id": 142,
    "phone_number": "+6281234567890",
    "amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "otp_code": "123456",
    "otp_received_at": "2026-02-25T10:05:00+00:00",
    "expires_at": "2026-02-25T10:20:00+00:00",
    "canceled_at": null,
    "failed_reason": null
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2: money fields are USD money objects and the response carries a single meta.fx { pair, rate, rate_as_of }. rate is whole IDR per 1 USD, so USD = canonical_amount / rate. Totals use 2 decimals; per-item prices/refunds use 4. A strictly-positive amount never rounds to 0.00. rate_as_of is the rate's RFC3339 timestamp (+00:00 form) or null when no timestamp is recorded.

v2 only: if no usable USD/IDR rate exists, money endpoints return 503 FX_RATE_UNAVAILABLE with a Retry-After header instead of a money body. v1 never returns this.

GET /orders/active

List all currently active orders (ACTIVE + OTP_RECEIVED). Use this to poll for OTP status updates.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v2/orders/active \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/orders/active", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders/active",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "OTP_RECEIVED",
      "otp_code": "123456",
      "otp_message": "Your verification code is 123456",
      "otp_received_at": "2026-02-25T10:05:00+00:00",
      "expires_at": "2026-02-25T10:20:00+00:00",
      "failed_reason": null
    },
    {
      "id": 1002,
      "status": "ACTIVE",
      "otp_code": null,
      "otp_message": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:50:00+00:00",
      "failed_reason": null
    }
  ]
}

v2: this endpoint is not money-bearing — it returns no amount and no meta.fx (same shape as v1, under /v2).

POST /orders/create

Creates a new virtual number order. Deducts balance automatically. Supports an optional Idempotency-Key header to prevent duplicate orders on network retries.

Request Body

NameTypeRequiredDescription
product_idintegerYesID of the product to order
quantityintegerNoNumber of items (1-100, default 1)

Pass an Idempotency-Key header (any unique string) to safely retry requests without creating duplicate orders.

Example Request

curl -s -X POST https://api.smscode.gg/v2/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-123" \
  -d '{"product_id":142,"quantity":1}'
const res = await fetch("https://api.smscode.gg/v2/orders/create", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Idempotency-Key": "unique-request-id-123",
  },
  body: JSON.stringify({ product_id: 142, quantity: 1 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/create",
    json={"product_id": 142, "quantity": 1},
    headers={
        "Authorization": "Bearer YOUR_API_TOKEN",
        "Idempotency-Key": "unique-request-id-123",
    })
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": 1002,
        "status": "ACTIVE",
        "phone_number": "+6281234567891",
        "otp_code": null,
        "otp_received_at": null,
        "expires_at": "2026-02-25T10:50:00+00:00",
        "failed_reason": null
      }
    ],
    "failed_count": 0
  }
}

Identical to v1 — only the base path changes (/v1/v2).

POST /orders/cancel

Cancel an active order. The rental cost is refunded to your account balance.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to cancel

Example Request

curl -s -X POST https://api.smscode.gg/v2/orders/cancel \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/cancel", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/cancel",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "new_balance": { "amount": "31.69", "currency": "USD", "canonical_amount": 515000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2: money fields are USD money objects and the response carries a single meta.fx { pair, rate, rate_as_of }. rate is whole IDR per 1 USD, so USD = canonical_amount / rate. Totals use 2 decimals; per-item prices/refunds use 4. A strictly-positive amount never rounds to 0.00. rate_as_of is the rate's RFC3339 timestamp (+00:00 form) or null when no timestamp is recorded.

v2 only: if no usable USD/IDR rate exists, money endpoints return 503 FX_RATE_UNAVAILABLE with a Retry-After header instead of a money body. v1 never returns this.

POST /orders/finish

Mark an order as completed after receiving the OTP. This releases the number immediately instead of waiting for expiry.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to finish

Example Request

curl -s -X POST https://api.smscode.gg/v2/orders/finish \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/finish", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/finish",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "COMPLETED"
  }
}

Identical to v1 — only the base path changes (/v1/v2).

POST /orders/resend

Request the platform to resend the SMS to the rented number. Not all platforms support resending — check the resent field in the response.

Request Body

NameTypeRequiredDescription
idintegerYesOrder ID to resend SMS for

Example Request

curl -s -X POST https://api.smscode.gg/v2/orders/resend \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/resend", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/resend",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "ACTIVE",
    "resent": true
  }
}

Identical to v1 — only the base path changes (/v1/v2).

GET /webhook

Returns your current webhook notification configuration.

Parameters

None

Example Request

curl -s https://api.smscode.gg/v2/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/webhook", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/webhook",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}

Identical to v1 — only the base path changes (/v1/v2).

PATCH /webhook

Update your webhook URL and/or secret. A secret is auto-generated when you set a URL for the first time. Send an empty string to clear. URL must use HTTPS.

Request Body

NameTypeRequiredDescription
webhook_urlstringNoHTTPS URL to receive webhook events (empty string to clear)
webhook_secretstringNoShared secret for HMAC-SHA256 signature (auto-generated if omitted on first set)

At least one field is required.

Example Request

curl -s -X PATCH https://api.smscode.gg/v2/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com/webhook"}'
const res = await fetch("https://api.smscode.gg/v2/webhook", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhook_url: "https://example.com/webhook" }),
});
const data = await res.json();
import requests

res = requests.patch("https://api.smscode.gg/v2/webhook",
    json={"webhook_url": "https://example.com/webhook"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}

Identical to v1 — only the base path changes (/v1/v2).

POST /webhook/test

Send a test event to your configured webhook URL. Returns the HTTP status code from your server. Useful for verifying your endpoint is working before going live.

Parameters

None

Example Request

curl -s -X POST https://api.smscode.gg/v2/webhook/test \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/webhook/test", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/webhook/test",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Example Response

200 OK
{
  "success": true,
  "data": {
    "status_code": 200
  }
}

Identical to v1 — only the base path changes (/v1/v2).

Webhook Notifications

Configure a webhook URL to receive real-time push notifications for order events instead of polling. This is the recommended approach for bot scripts.

Events

EventTrigger
order.otp_receivedOTP code delivered to the rented number
order.completedOrder marked as completed (manually or by expiry)
order.expiredOrder expired without OTP (balance refunded)
order.canceledOrder canceled by user (balance refunded)

Payload

Webhook POST Body
{
  "event": "order.otp_received",
  "timestamp": "2026-02-25T12:00:00+00:00",
  "data": {
    "order_id": 1001,
    "phone_number": "+628123456789",
    "otp_code": "1234",
    "otp_message": "Your verification code is 1234",
    "product_id": 42,
    "country": "Indonesia",
    "platform": "WhatsApp"
  }
}

Signature Verification

Each webhook request includes an X-Webhook-Signature header with an HMAC-SHA256 signature of the request body, using your webhook_secret as the key:

X-Webhook-Signature: sha256={HMAC-SHA256(body, webhook_secret)}

Verify this signature on your server to ensure the request is authentic. Delivery is fire-and-forget with a 3-second timeout and no retries.

Rate Limits

API requests are rate-limited per endpoint group. Exceeding the limit returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.

Endpoint GroupLimitWindow
Catalog (countries, services, products, exchange-rate)5,000 requests60 seconds
Balance600 requests60 seconds
Order reads (list, get, active)5,000 requests60 seconds
Order create3,000 requests60 seconds
Order cancel1,000 requests60 seconds
Order actions (finish, resend)1,000 requests60 seconds
Webhook config (get, update)600 requests60 seconds
Webhook test10 requests60 seconds

Error Codes

Error responses include one of these codes in error.code:

CodeHTTPDescription
UNAUTHORIZED401Missing or invalid API token
FORBIDDEN403Access denied
NOT_FOUND404Resource not found (order, exchange rate, etc.)
CONFLICT409Duplicate request or resource conflict
INSUFFICIENT_BALANCE409Not enough balance to create order
VALIDATION_ERROR422Request parameters failed validation
RATE_LIMIT_EXCEEDED429Too many requests (check Retry-After header)
INTERNAL_ERROR500Internal server error
PROVIDER_ERROR422Upstream SMS provider rejected the request
CANCEL_TOO_EARLY409Order too recent to cancel — wait 2 minutes
SERVICE_UNAVAILABLE503Service temporarily unavailable (maintenance)
FX_RATE_UNAVAILABLE503USD/IDR exchange rate unavailable (v2 money endpoints) — returns 503 with a Retry-After header.
v1 → v2

Migrating v1 → v2

v1 serves IDR; v2 serves USD. Both versions coexist permanently — there is no sunset. Pick one version per integration; do not mix base paths. v2 is identical to v1 except how money is represented.

Aspectv1 · IDRv2 · USD
Money fieldsInteger IDR, e.g. 15000Money object { amount, currency, canonical_amount, canonical_currency }
meta.fxAbsentRequired on every money-bearing response
CurrencyIDRUSD (hardcoded)
FX_RATE_UNAVAILABLENew 503 + Retry-After when no usable rate
PrecisionTotals 2dp, prices/refunds 4dp, positive-floor
GET /catalog/exchange-rate{pair, base_currency, quote_currency, rate}; honors ?pair{pair, rate, rate_as_of}; ?pair ignored (USD/IDR only)

Side-by-side examples

GET/balance
v1
{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 500000
  }
}
v2
{
  "success": true,
  "data": {
    "balance": { "amount": "30.77", "currency": "USD", "canonical_amount": 500000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}
GET/catalog/products
v1
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "available": 42,
      "price": 15000,
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1 }
}
v2
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "available": 42,
      "price": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1, "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}
POST/orders/cancel
v1
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": 15000,
    "new_balance": 515000
  }
}
v2
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "new_balance": { "amount": "31.69", "currency": "USD", "canonical_amount": 515000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

Migration checklist

  1. Switch the base path /v1/v2.
  2. Parse money fields as objects — read amount as a decimal string; currency is "USD".
  3. For ledger reconciliation use canonical_amount (exact IDR); the USD amount is a render-time projection and the rate is disclosed once in meta.fx.
  4. Handle the new FX_RATE_UNAVAILABLE (503) — retry after Retry-After. v1 never returns this.