API Documentation
kiwiSMS REST API v1. Create an API key in your dashboard. Base URL: your site origin (e.g. https://your-domain.com).
OpenAPI spec: /openapi.json · /openapi.yaml · pricing catalog
Authentication
Send your API key on every authenticated request:
X-API-Key: lsms_xxxxxxxx_your_secret # or Authorization: Bearer lsms_xxxxxxxx_your_secret
Service codes
Each catalog row has three related fields:
kind— line type:s1,s2,s3,s4, ortm1serviceNo— integer within that kind (starts at 1)code— display referencekind-serviceNo(e.g.s1-5) for short-term- Long-term plans use
kind(l1,l2,l3),planNo, andcode(e.g.l2-3)
When renting, send kind + serviceNo from GET /api/v1/services. The code field is for display and search only.
Recommended flow
GET /api/v1/services— pick a service with stock (notecode)GET /api/v1/balance— check walletPOST /api/v1/rentals— rent withkind+serviceNo- Poll
GET /api/v1/rentals/{id}untilcodesis non-empty, or use webhooks POST .../donewhen finished
Endpoints
/api/v1/balanceWallet balance (USD).
Response
{ "balance": 12.5, "currency": "USD" }/api/v1/services?country=187Full authenticated catalog with live stock. Optional country code (default 187 = US).
Response
{
"countryCode": "187",
"catalogMode": true,
"count": 42,
"services": [
{
"kind": "s1",
"serviceNo": 5,
"code": "s1-5",
"name": "Google",
"price": "0.12",
"count": 120,
"multiSms": true,
"durationMinutes": 20
}
]
}/api/public/services?country=187&page=1&pageSize=100&q=googlePublic paginated catalog (no auth). Max 100 items per page. Search by name or code.
Response
{
"countryCode": "187",
"services": [{ "kind": "s1", "serviceNo": 5, "code": "s1-5", "name": "Google", "price": "0.12", "count": 120, "multiSms": true, "durationMinutes": 20 }],
"total": 1,
"page": 1,
"pageSize": 100,
"totalPages": 1,
"q": "google"
}/api/public/long-term-plans?page=1&pageSize=100&q=googlePublic paginated long-term catalog (no auth). Max 100 per page.
Response
{
"available": true,
"plans": [{
"kind": "l2",
"planNo": 3,
"code": "l2-3",
"serviceName": "Google",
"countryName": null,
"durationLabel": "30 days",
"durationKey": "30d",
"price": "5.00",
"renewPrice": "4.00"
}],
"total": 1,
"page": 1,
"pageSize": 100,
"totalPages": 1
}/api/v1/long-term/plans?page=1&pageSize=100Authenticated long-term plan catalog with pagination and search.
/api/v1/long-term/rentals?sync=1List your long-term rentals. sync=0 skips provider sync.
/api/v1/long-term/rentalsRent a long-term number using kind + planNo from the catalog.
Request
{
"kind": "l2",
"planNo": 3,
"area_code": "212",
"quantity": 1
}Response
{
"rental": {
"id": "clx...",
"kind": "l2",
"planNo": 3,
"code": "l2-3",
"serviceName": "Google",
"phone": "+12025551234",
"sellPrice": 5.0,
"status": "ACTIVE",
"lineStatus": "active"
}
}/api/v1/long-term/rentals/{id}?sync=1Poll one long-term rental.
/api/public/pricingSmall public snapshot for landing page (top services, no auth).
/api/v1/rentals?sync=1Active rentals (WAITING/RECEIVED). sync=0 skips SMS sync (faster).
Response
{
"count": 1,
"rentals": [{
"id": 1042,
"kind": "s1",
"serviceNo": 5,
"phone": "+12025551234",
"serviceName": "Google",
"status": "RECEIVED",
"codes": ["123456"],
"otpCode": "123456",
"secondsRemaining": 840,
"active": true
}]
}/api/v1/rentals/{id}?sync=1Poll one rental — same shape as list item. Syncs SMS when still active.
/api/v1/rentalsRent a number (~20 min). Charges wallet. Send Idempotency-Key header for safe retries.
Request
Headers:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"kind": "s1",
"serviceNo": 5,
"max_price": "0.50",
"areas": "212,718",
"carrier": "tmo",
"country": "187"
}Response
{
"rental": {
"id": 1042,
"kind": "s1",
"phone": "+12025551234",
"serviceName": "Google",
"price": 0.12,
"status": "WAITING",
"expiresAt": "2026-05-29T12:00:00.000Z"
}
}/api/v1/rentals/{id}/cancelCancel WAITING rental — refunds wallet.
/api/v1/rentals/{id}/doneMark rental completed (release the number).
/api/v1/rentals/{id}/renewRenew same number (new session). Use the KiwiSMS rental id from a previous rental.
Testing the API
Replace placeholders with your domain and API key:
# List services
curl -s -H "X-API-Key: lsms_xxx_your_secret" \
"https://your-domain.com/api/v1/services?country=187" | jq .
# Check balance
curl -s -H "X-API-Key: lsms_xxx_your_secret" \
"https://your-domain.com/api/v1/balance" | jq .
# Rent (use kind + serviceNo from catalog)
curl -s -X POST -H "Content-Type: application/json" \
-H "X-API-Key: lsms_xxx_your_secret" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"s1","serviceNo":5,"country":"187"}' \
"https://your-domain.com/api/v1/rentals" | jq .
# Poll rental
curl -s -H "X-API-Key: lsms_xxx_your_secret" \
"https://your-domain.com/api/v1/rentals/1042?sync=1" | jq .
# Long-term plans
curl -s -H "X-API-Key: lsms_xxx_your_secret" \
"https://your-domain.com/api/v1/long-term/plans?page=1&pageSize=100" | jq .
# Rent long-term (kind + planNo from catalog)
curl -s -X POST -H "Content-Type: application/json" \
-H "X-API-Key: lsms_xxx_your_secret" \
-d '{"kind":"l2","planNo":3}' \
"https://your-domain.com/api/v1/long-term/rentals" | jq .Public catalog (no key): GET /api/public/services?page=1&pageSize=100&q=google
Idempotency
For POST /api/v1/rentals and POST .../renew, send header Idempotency-Key: <uuid> (max 256 chars). Reusing the same key with the same body within 24 hours returns the original response instead of creating a second charge.
IDEMPOTENCY_IN_PROGRESS— 409, same key still processingIDEMPOTENCY_KEY_REUSED— 422, same key but different request body
Errors
JSON body { error, code? }. Common codes:
INSUFFICIENT_BALANCE— 402SERVICE_UNAVAILABLE— 404RENTAL_NOT_FOUND— 404CANCEL_NOT_ALLOWED— not WAITING
Rate limits
Per API key (default 100 requests/minute). Public endpoints are limited per IP. HTTP 429 when exceeded.
Webhooks
Set webhook URL and secret on your account or API key. When an OTP arrives, kiwiSMS POSTs JSON to your URL:
{
"event": "sms.received",
"rentalId": "1042",
"kind": "s1",
"serviceNo": 5,
"code": "s1-5",
"serviceName": "Google",
"phone": "+12025551234",
"otpCode": "123456",
"smsText": "Your Google verification code is 123456",
"receivedAt": "2026-05-29T12:00:00.000Z"
}Header X-Webhook-Signature = HMAC-SHA256 hex digest of the raw request body using your webhook secret. Respond with HTTP 2xx to acknowledge.
Verify (Node.js)
import crypto from "crypto";
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader.trim(), "hex"),
Buffer.from(expected, "hex")
);
}
// In Express: use express.raw({ type: "application/json" }) before json parser
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const sig = req.headers["x-webhook-signature"];
if (!verifyWebhook(raw, process.env.WEBHOOK_SECRET, sig)) {
return res.status(401).send("invalid signature");
}
const payload = JSON.parse(raw);
console.log(payload.otpCode, payload.code);
res.sendStatus(200);
});Verify (Python)
import hmac
import hashlib
def verify_webhook(raw_body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature.strip())
# Flask example
@app.post("/webhook")
def webhook():
raw = request.get_data()
sig = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(raw, WEBHOOK_SECRET, sig):
return "invalid signature", 401
payload = request.json
return "", 200Test locally: point your webhook URL to a tunnel (e.g. ngrok) and rent a number; the payload includes otpCode and code.