getotp API
Buy numbers, read verification codes and check your balance from your own code. All endpoints return JSON, use HTTPS, and need an API key created in API & Developer.
Authentication
Send your key as a bearer token. Keys are scoped — a request without the needed scope gets 403. Limit: 120 requests per minute per key (429 with Retry-After).
Authorization: Bearer gotp_live_…Endpoints
/api/v1/catalog?country=USscope: catalog:readServices and prices. yourPriceMinor includes your plan discount.
/api/v1/balancescope: balance:readWallet balance in minor units (cents).
/api/v1/ordersscope: orders:writeBuy a number. Requires an Idempotency-Key header.
/api/v1/ordersscope: orders:readYour recent orders (?limit=1–100).
/api/v1/orders/{id}scope: orders:readOne order with its messages and verification codes. Poll this, or use a webhook.
/api/v1/orders/{id}scope: orders:writeCancel an order with no SMS yet and get an automatic refund.
Buying a number
Always send a unique Idempotency-Key (16–128 characters). If your request times out, retry with the same key — you will never be charged twice. A 202 response means the provider outcome is being confirmed; do not buy again, poll the order instead. Amounts are in minor units (cents, USD).
curl -X POST https://your-domain.com/api/v1/orders \
-H "Authorization: Bearer gotp_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"serviceId":"<id from the catalog>","countryCode":"US","maxPriceMinor":50}'Webhooks
Set an https URL in API & Developer. For every new SMS we POST this JSON, signed with your endpoint secret in the GetOTP-Signature header. Delivery is best effort with a 4 second timeout and no retries — always keep polling as a fallback. Webhook URLs must be public https hosts.
{
"type": "sms.received",
"orderId": "…",
"serviceName": "WhatsApp",
"countryCode": "US",
"phoneNumber": "+15672345678",
"message": { "id": "…", "sender": "WhatsApp", "body": "Your code is 123-456", "code": "123456", "receivedAt": "2026-01-01T12:00:00.000Z" }
}Verify the signature against the raw request body:
import { createHmac, timingSafeEqual } from 'node:crypto';
// header looks like: t=1700000000,v1=<hex>
export function verify(secret, rawBody, header) {
const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // reject replays
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Errors
Errors look like { "error": "message" }. 400 bad input · 401 bad key · 402 insufficient balance · 403 missing scope · 409 price or availability changed · 429 rate or daily-order limit · 503 temporarily unavailable.