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:
Requests without a valid token receive a 401 UNAUTHORIZED response.
⟩Base URL
All endpoint paths below are relative to:
⟩Response Format
Every response returns JSON with a consistent envelope. All responses include an x-request-id header for debugging.
{
"success": true,
"data": { ... }
} {
"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.
/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
{
"success": true,
"data": [
{
"id": 6,
"code": "ID",
"name": "Indonesia",
"dial_code": "+62",
"emoji": "🇮🇩",
"active": true
}
]
} /catalog/services Returns a list of available services (platforms). Optionally filter by country.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
country_id | integer | No | Filter 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
{
"success": true,
"data": [
{
"id": 3,
"code": "wa",
"name": "WhatsApp",
"active": true
}
]
} /catalog/products Returns a paginated list of available products. Filter by country and/or platform.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
country_id | integer | No | Filter by country ID |
platform_id | integer | No | Filter by platform/service ID |
sort | string | No | Sort order: price_asc (default), price_desc, available_asc, available_desc, name_asc, name_desc |
limit | integer | No | Results per page (1-10,000, default 1,000) |
page | integer | No | Page 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
{
"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 }
} /catalog/exchange-rate Returns the current USD/IDR exchange rate used for currency conversion.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
pair | string | No | Currency 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
{
"success": true,
"data": {
"pair": "USD/IDR",
"base_currency": "USD",
"quote_currency": "IDR",
"rate": 16250
}
} /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
{
"success": true,
"data": {
"currency": "IDR",
"balance": 500000
}
} /orders Returns a list of the authenticated user's orders, sorted by most recent. Supports filtering by status and pagination via offset.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Max results (1-100, default 20) |
offset | integer | No | Number of results to skip (default 0) |
status | string | No | Filter 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
{
"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
}
]
} /orders/{id} Returns a single order by ID. Only returns orders owned by the authenticated user.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"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
}
} /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
{
"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
}
]
} /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
| Name | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | ID of the product to order |
quantity | integer | No | Number 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
{
"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
}
} /orders/cancel Cancel an active order. The rental cost is refunded to your account balance.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"success": true,
"data": {
"order_id": 1001,
"status": "CANCELED",
"refund_amount": 15000,
"new_balance": 515000
}
} /orders/finish Mark an order as completed after receiving the OTP. This releases the number immediately instead of waiting for expiry.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"success": true,
"data": {
"order_id": 1001,
"status": "COMPLETED"
}
} /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
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"success": true,
"data": {
"order_id": 1001,
"status": "ACTIVE",
"resent": true
}
} /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
{
"success": true,
"data": {
"webhook_url": "https://example.com/webhook",
"webhook_secret": "a1b2c3d4e5f6..."
}
} /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
| Name | Type | Required | Description |
|---|---|---|---|
webhook_url | string | No | HTTPS URL to receive webhook events (empty string to clear) |
webhook_secret | string | No | Shared 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
{
"success": true,
"data": {
"webhook_url": "https://example.com/webhook",
"webhook_secret": "a1b2c3d4e5f6..."
}
} /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
{
"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
| Event | Trigger |
|---|---|
order.otp_received | OTP code delivered to the rented number |
order.completed | Order marked as completed (manually or by expiry) |
order.expired | Order expired without OTP (balance refunded) |
order.canceled | Order canceled by user (balance refunded) |
Payload
{
"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:
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 Group | Limit | Window |
|---|---|---|
| Catalog (countries, services, products, exchange-rate) | 5,000 requests | 60 seconds |
| Balance | 600 requests | 60 seconds |
| Order reads (list, get, active) | 5,000 requests | 60 seconds |
| Order create | 3,000 requests | 60 seconds |
| Order cancel | 1,000 requests | 60 seconds |
| Order actions (finish, resend) | 1,000 requests | 60 seconds |
| Webhook config (get, update) | 600 requests | 60 seconds |
| Webhook test | 10 requests | 60 seconds |
⟩Error Codes
Error responses include one of these codes in error.code:
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API token |
FORBIDDEN | 403 | Access denied |
NOT_FOUND | 404 | Resource not found (order, exchange rate, etc.) |
CONFLICT | 409 | Duplicate request or resource conflict |
INSUFFICIENT_BALANCE | 409 | Not enough balance to create order |
VALIDATION_ERROR | 422 | Request parameters failed validation |
RATE_LIMIT_EXCEEDED | 429 | Too many requests (check Retry-After header) |
INTERNAL_ERROR | 500 | Internal server error |
PROVIDER_ERROR | 422 | Upstream SMS provider rejected the request |
CANCEL_TOO_EARLY | 409 | Order too recent to cancel — wait 2 minutes |
SERVICE_UNAVAILABLE | 503 | Service 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.
⟩Authentication
All API requests require a Bearer token. Generate one from Account Settings in the dashboard, then include it in every request:
Requests without a valid token receive a 401 UNAUTHORIZED response.
⟩Base URL
All endpoint paths below are relative to:
⟩Response Format
Every response returns JSON with a consistent envelope. All responses include an x-request-id header for debugging.
{
"success": true,
"data": { ... }
} {
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message"
}
} v2 error: no usable exchange rate (503)
{
"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.
/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
{
"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).
/catalog/services Returns a list of available services (platforms). Optionally filter by country.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
country_id | integer | No | Filter 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
{
"success": true,
"data": [
{
"id": 3,
"code": "wa",
"name": "WhatsApp",
"active": true
}
]
} Identical to v1 — only the base path changes (/v1 → /v2).
/catalog/products Returns a paginated list of available products. Filter by country and/or platform.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
country_id | integer | No | Filter by country ID |
platform_id | integer | No | Filter by platform/service ID |
sort | string | No | Sort order: price_asc (default), price_desc, available_asc, available_desc, name_asc, name_desc |
limit | integer | No | Results per page (1-10,000, default 1,000) |
page | integer | No | Page 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
{
"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.
/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
{
"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.
/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
{
"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.
/orders Returns a list of the authenticated user's orders, sorted by most recent. Supports filtering by status and pagination via offset.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Max results (1-100, default 20) |
offset | integer | No | Number of results to skip (default 0) |
status | string | No | Filter 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
{
"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.
/orders/{id} Returns a single order by ID. Only returns orders owned by the authenticated user.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"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.
/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
{
"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).
/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
| Name | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | ID of the product to order |
quantity | integer | No | Number 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
{
"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).
/orders/cancel Cancel an active order. The rental cost is refunded to your account balance.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"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.
/orders/finish Mark an order as completed after receiving the OTP. This releases the number immediately instead of waiting for expiry.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"success": true,
"data": {
"order_id": 1001,
"status": "COMPLETED"
}
} Identical to v1 — only the base path changes (/v1 → /v2).
/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
| Name | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Order 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
{
"success": true,
"data": {
"order_id": 1001,
"status": "ACTIVE",
"resent": true
}
} Identical to v1 — only the base path changes (/v1 → /v2).
/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
{
"success": true,
"data": {
"webhook_url": "https://example.com/webhook",
"webhook_secret": "a1b2c3d4e5f6..."
}
} Identical to v1 — only the base path changes (/v1 → /v2).
/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
| Name | Type | Required | Description |
|---|---|---|---|
webhook_url | string | No | HTTPS URL to receive webhook events (empty string to clear) |
webhook_secret | string | No | Shared 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
{
"success": true,
"data": {
"webhook_url": "https://example.com/webhook",
"webhook_secret": "a1b2c3d4e5f6..."
}
} Identical to v1 — only the base path changes (/v1 → /v2).
/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
{
"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
| Event | Trigger |
|---|---|
order.otp_received | OTP code delivered to the rented number |
order.completed | Order marked as completed (manually or by expiry) |
order.expired | Order expired without OTP (balance refunded) |
order.canceled | Order canceled by user (balance refunded) |
Payload
{
"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:
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 Group | Limit | Window |
|---|---|---|
| Catalog (countries, services, products, exchange-rate) | 5,000 requests | 60 seconds |
| Balance | 600 requests | 60 seconds |
| Order reads (list, get, active) | 5,000 requests | 60 seconds |
| Order create | 3,000 requests | 60 seconds |
| Order cancel | 1,000 requests | 60 seconds |
| Order actions (finish, resend) | 1,000 requests | 60 seconds |
| Webhook config (get, update) | 600 requests | 60 seconds |
| Webhook test | 10 requests | 60 seconds |
⟩Error Codes
Error responses include one of these codes in error.code:
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API token |
FORBIDDEN | 403 | Access denied |
NOT_FOUND | 404 | Resource not found (order, exchange rate, etc.) |
CONFLICT | 409 | Duplicate request or resource conflict |
INSUFFICIENT_BALANCE | 409 | Not enough balance to create order |
VALIDATION_ERROR | 422 | Request parameters failed validation |
RATE_LIMIT_EXCEEDED | 429 | Too many requests (check Retry-After header) |
INTERNAL_ERROR | 500 | Internal server error |
PROVIDER_ERROR | 422 | Upstream SMS provider rejected the request |
CANCEL_TOO_EARLY | 409 | Order too recent to cancel — wait 2 minutes |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable (maintenance) |
FX_RATE_UNAVAILABLE | 503 | USD/IDR exchange rate unavailable (v2 money endpoints) — returns 503 with a Retry-After header. |
⟩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.
| Aspect | v1 · IDR | v2 · USD |
|---|---|---|
| Money fields | Integer IDR, e.g. 15000 | Money object { amount, currency, canonical_amount, canonical_currency } |
meta.fx | Absent | Required on every money-bearing response |
| Currency | IDR | USD (hardcoded) |
FX_RATE_UNAVAILABLE | — | New 503 + Retry-After when no usable rate |
| Precision | — | Totals 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
{
"success": true,
"data": {
"currency": "IDR",
"balance": 500000
}
}{
"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" } }
}{
"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 }
}{
"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" } }
}{
"success": true,
"data": {
"order_id": 1001,
"status": "CANCELED",
"refund_amount": 15000,
"new_balance": 515000
}
}{
"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
- Switch the base path
/v1→/v2. - Parse money fields as objects — read
amountas a decimal string;currencyis"USD". - For ledger reconciliation use
canonical_amount(exact IDR); the USDamountis a render-time projection and therateis disclosed once inmeta.fx. - Handle the new
FX_RATE_UNAVAILABLE(503) — retry afterRetry-After. v1 never returns this.