API Documentation

Complete REST API reference for OTPZap virtual number and OTP verification integration.

Connection Info

Base URLhttps://otpzap.com/api/v1
AuthenticationAuthorization: Bearer YOUR_API_KEY
Rate Limit300 requests/minute per API key
FormatJSON (request & response)

Quick Guide

OTPZap supports 3 servers with different workflows:

Account & Balance

GET/balance

Check account balance.

Response: {"success":true,"data":{"balance":50000,"currency":"IDR"}}
POST/topup/create

Create a QRIS deposit invoice (rupiah). Redirect the customer to payment_url to complete payment. Balance is credited automatically on successful payment. Invoice valid for 30 minutes. Send a unique Idempotency-Key header to avoid duplicate invoices on retry.

Body: {"amount":50000}
Response: {"success":true,"data":{"order_id":"APIDEP-3-xxx","amount":50000,"total_amount":50017,"payment_url":"https://otpzap.com/pay/APIDEP-3-xxx","expired_at":"2026-05-19 10:30:00 WIB"}}
GET/topup/check?order_id=APIDEP-3-xxx

Check deposit status. States: pending | success | expired | failed. Polling also drives reconciliation automatically.

Response: {"success":true,"data":{"order_id":"APIDEP-3-xxx","amount":50000,"status":"success","paid_at":"2026-05-19 09:45:12 WIB","created_at":"2026-05-19 09:30:00 WIB"}}

🪙 Crypto Payments

Crypto deposits (BTC, USDT, ETH, USDC, etc.) are currently available through the web dashboard only - not exposed via the Public API. Customers can choose crypto payment directly on the Deposit Balance page in the dashboard. Crypto payment window: 25 minutes after invoice creation.

Server 1

Use product_id from the /products endpoint. 1 product = 1 service + 1 country + 1 price tier. Catalog data is fetched fresh by OTPZap; fetch again before creating an order and avoid long client-side caching.

GET/countries

List of available countries (Server 1).

Response: {"success":true,"data":{"countries":[{"id":7,"name":"Indonesia"}]}}
GET/services?country_id=7

List of platforms/services. Optional parameter: country_id.

Response: {"success":true,"data":{"services":[{"id":1,"code":"whatsapp","name":"WhatsApp"}]}}
GET/products?platform_id=1&country_id=7

List of products + pricing + stock. Parameters: platform_id, country_id, page, limit, sort.

Response: {"success":true,"data":{"products":[{"id":814090256,"name":"WhatsApp - Indonesia","price":2835,"stock":547}]}}

Server 2

Use service_id + country_id + operator_id. Supports multiservice and special services. Service data is also fresh from OTPZap.

GET/s2/countries

List of available countries (Server 2).

Response: {"success":true,"data":{"countries":[{"id":7,"name":"Indonesia"}]}}
GET/s2/services?country_id=7

List of services + pricing per country.

Response: {"success":true,"data":{"services":[{"service_id":101,"service_name":"WhatsApp","price":1500}]}}
GET/s2/operators?country_id=7

List of operators per country.

Response: {"success":true,"data":{"operators":["random","telkomsel","indosat"]}}
GET/s2/special

List of special services (no country/operator parameter required). These services use pre-allocated virtual numbers dedicated to specific platforms.

Response: {"success":true,"data":{"services":[{"service_id":501,"service_name":"Gojek Special","price":3500}]}}

Server 3 - Email OTP

Use Server 3 for email inbox rental. First choose a target site and domain, then create an email order. The first 20 minutes are the waiting window; after a message arrives, session lifetime follows the email domain/service policy.

GET/s3/sites

Suggested target sites. Suggestions only; any valid site domain can be sent to the create endpoint.

Response: {"success":true,"data":{"sites":[{"site":"openai.com","name":"OpenAI / ChatGPT","category":"ai"}],"total":30}}
GET/s3/domains?site=openai.com

Available email domains, stock, price, activation rate, and cancellation fee preview.

Response: {"success":true,"data":{"domains":[{"name":"gmail.com","price_idr":12000,"stock":25,"cancel_fee_idr":0}]}}
POST/order/email-create

Buy Email OTP. Optional fields: qty, subject, regex, or domains[] for aggregator mode.

Body: {"site":"openai.com","domain":"gmail.com","qty":1}
Response: {"success":true,"data":{"order":{"order_id":88,"ref":"APIE-3-xxx","server":3,"order_type":"email","email":"[email protected]","status":"pending"}}}
GET/order/email-check?ref=APIE-3-xxx

Poll email status and messages. Returns inbox preview, extracted OTP/headline, full message timeline, and updated expires_at.

POST/order/email-reorder

Request a new message on the same email inbox. The waiting window resets to 20 minutes and OTPZap balance is not debited again.

Body: {"ref":"APIE-3-xxx"}
POST/order/email-cancel

Cancel before any message arrives. Refund can be reduced by the system cancellation fee.

Body: {"ref":"APIE-3-xxx"}
POST/order/email-finish

Finish an email order after a message arrives and close the activation window.

Body: {"ref":"APIE-3-xxx"}

Order Management

The following endpoints apply to SMS orders on Server 1 and Server 2. Email OTP uses the dedicated Server 3 endpoints above.

POST/order/create

Buy an OTP number. Optional header Idempotency-Key prevents duplicate orders on retry. Use IDs from the latest catalog response. Optional service_name must match the service name for the selected ID when sent.

Server 1: {"server":1,"product_id":814090256,"country_id":7,"platform_id":1}
Server 2 regular: {"server":2,"service_id":101,"country_id":7,"operator_id":"random"}
Server 2 special: {"server":2,"service_id":501,"is_special":true}
Server 2 multiservice: {"server":2,"service_ids":[101,102,103],"country_id":7,"operator_id":"random"}
Response (single): {"success":true,"data":{"order_id":65,"ref":"API3-xxx","server":1,"phone":"628xxx","service":"WhatsApp","price":2835,"currency":"IDR","status":"pending","multiservice":false,"created_at":"..."}}
GET/order/check?order_id=65

Check status & fetch OTP. Poll this endpoint every 5 seconds. Status flow: pendingotp_received (S1, number still active) → success. Failed orders are auto-refunded.

Response: {"success":true,"data":{"order_id":65,"ref":"API3-xxx","server":1,"phone":"628xxx","service":"WhatsApp","otp_code":"123456","otps":[{"code":"123456","received_at":"2026-05-19 10:35:12 WIB"}],"price":2835,"currency":"IDR","status":"otp_received","resend_count":0,"last_resend_at":null,"resend_locked_until":null,"created_at":"2026-05-19 10:30:00 WIB","updated_at":"2026-05-19 10:35:12 WIB"}}

Key fields: otp_code is the latest/final OTP for automated integrations. otps[] is the timeline of all OTPs received (oldest first), so do not use the first array item as the main OTP. Useful when resend adds a new OTP - you can detect a new entry by array length change. resend_count = how many resends used. resend_locked_until = WIB timestamp until resend is unlocked (null if available).

POST/order/cancel

Cancel a pending order. A 2-minute cooldown applies to all orders (single & multiservice). Balance is refunded automatically when system confirms cancellation. Error codes: 409 if status is not pending, 429 if cooldown not yet elapsed (check Retry-After header), 502 if system rejects.

Body: {"order_id":65}
Response (success): {"success":true,"data":{"order_id":65,"refunded":2835}}
POST/order/finish

Mark order as finished. Use after the OTP is received and the number is no longer needed.

Body: {"order_id":65}
POST/order/resend

Resend SMS. Per-OTP gating: after one resend, the next call returns HTTP 429 until a new OTP arrives through polling. Status and existing OTP are not reset - new OTP appears as an additional entry in otps[]. Not all platforms support resend.

Body: {"order_id":65}
POST/order/replace

Server 1 only. Atomically cancel the current order and buy a fresh number at the same product/price. Old balance refunded, new balance deducted in one transaction.

Body: {"order_id":65}
Response: {"success":true,"data":{"order_id":99,"old_ref":"API3-xxx","ref":"API3-yyy","phone":"628yyy","price":2835,"status":"pending"}}

Webhook

OTPZap sends POST requests to your webhook URL when events occur. Configure via dashboard or the API endpoints below.

Events:

Webhook Management

GET/webhook/get

Fetch current webhook configuration. secret is never returned here (security - only shown once at set/update time).

Response (configured): {"success":true,"data":{"configured":true,"url":"https://yoursite.com/hook","events":["order.otp_received","order.finished"],"is_active":true,"updated_at":"2026-05-19 10:00:00 WIB"}}
Response (not set yet): {"success":true,"data":{"configured":false,"url":null,"events":[],"is_active":false}}
PATCH/webhook/update

Create or update webhook configuration. Method is PATCH (not POST). URL must be HTTPS and resolve to a public IP (private IPs are rejected for anti-SSRF protection).

Body: {"url":"https://yoursite.com/hook","events":["order.otp_received","order.finished","order.replaced"],"secret":"OPTIONAL_MIN_16_CHARS","is_active":true}
Response: {"success":true,"data":{"url":"https://yoursite.com/hook","secret":"abc123def456...","events":[...],"is_active":true,"note":"Save the secret now. It will not be shown again for security."}}

Optional fields: secret minimum 16 characters (if not provided or shorter, OTPZap auto-generates a 48-char hex). Save this secret somewhere safe - it will not be shown again at /webhook/get. is_active defaults to true; set to false to pause delivery without deleting config.

Error 422: URL is not HTTPS, URL resolves to private/loopback IP (anti-SSRF), or an event is not valid (see list above).

Webhook Security

Signature Verification:

// PHP $body = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $body, YOUR_SECRET); if (!hash_equals($expected, $_SERVER['HTTP_X_OTPZAP_SIGNATURE'])) die('Invalid'); // Node.js const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex'); if (req.headers['x-otpzap-signature'] !== expected) return res.status(401).end();

Need help? Contact us via Telegram.