Fast integration
From API key → number → OTP with copy/paste requests and expandable reference below.
✅ Trusted by 423,454+ users · ⭐ 4.1/5 on Trustpilot · 200+ countries✅ 423,454+ users · Trustpilot
Read FAQs →
Integrate faster: request numbers, receive OTP SMS, handle errors cleanly, and scale with polling best practices.
Two APIs, one key — the JSON REST API at https://api.pvapins.com/api/v1
and the classic API at https://api.pvapins.com/user/api/.
Responsible use: Follow local laws and each platform’s terms. PVAPins is not affiliated with third-party apps.
Minimal steps to your first OTP via API.

Predictable responses, clear flows, and practical guidance for production.
From API key → number → OTP with copy/paste requests and expandable reference below.
Separate temporary and rent logic so your code stays easier to debug.
Rate-limit-friendly polling, error mapping, and retry tips.
Replace YOUR_API_KEY, COUNTRY_ID, APP_ID, and NUMBER with your values.
curl -s 'https://api.pvapins.com/user/api/load_countries.php'curl -s 'https://api.pvapins.com/user/api/load_apps.php?country_id=COUNTRY_ID'curl -s 'https://api.pvapins.com/user/api/get_number.php?customer=YOUR_API_KEY&app=APP_NAME&country=COUNTRY_NAME'# Poll every 3–6s with backoff
curl -s 'https://api.pvapins.com/user/api/get_sms.php?customer=YOUR_API_KEY&number=NUMBER&country=COUNTRY_NAME&app=APP_NAME'Combine / operator (optional): For services that use a group name plus an operator suffix on the site, add &operator=OPERATOR_SUFFIX and set app to the group name (not the full internal name). Use the same pairing on get_number, get_sms, and reject endpoints. If you omit operator, app must stay the full app name, unchanged from before.
# get_number / get_sms / reject — use the same app + operator pair everywhere
curl -s 'https://api.pvapins.com/user/api/get_number.php?customer=YOUR_API_KEY&app=GROUP_NAME&country=COUNTRY_NAME&operator=OPERATOR_SUFFIX'Integration tip: Use exponential backoff (e.g. 3s → 5s → 8s) and stop after a timeout.
One key works on both APIs. Send it in a header — the ?customer= query form still works for existing integrations, but a key in a URL ends up in access logs, Referer headers and browser history, so don't build new code on it.
# preferred
curl -s -H 'X-API-Key: YOUR_API_KEY' 'https://api.pvapins.com/api/v1/account'
# also accepted
curl -s -H 'Authorization: Bearer YOUR_API_KEY' 'https://api.pvapins.com/api/v1/account'Register, open your dashboard API page, and copy the key. Use it only on your server.
Store keys in environment variables, rotate if leaked, and log requests in production. There is no CORS — never call these APIs from a browser.
The modern API: JSON in, JSON out, header authentication, ISO country codes and a machine-readable
OpenAPI 3.1 spec you can generate a client from. Base URL
https://api.pvapins.com. Everything below is also browsable and runnable in the
interactive reference.
Every response is JSON. Errors carry a stable machine code plus a human message.
GET /operators lists price and stock per operator, most reliable first. Pin one, or let us auto-pick.
Send an Idempotency-Key on order creation and a retry returns the original order instead of buying twice.
| Endpoint | What it does | Notes |
|---|---|---|
GET /api/v1/account | Your account and spendable balance | Balance already excludes funds held by pending orders |
GET /api/v1/countries | Supported countries | Keyed by ISO 3166-1 alpha-2 (US, GB) |
GET /api/v1/services | Supported services and their codes | Read codes from here — never guess them |
GET /api/v1/operators | Operators, price and stock for a country + service | Requires country and service. Most reliable first |
GET /api/v1/numbers | Live availability and price | price is filled only when both country and service are given |
POST /api/v1/orders | Reserve a number | This is the call that spends money |
GET /api/v1/orders | Your 20 most recent orders | Not paginated — don't reconcile on this alone |
GET /api/v1/orders/{id} | Poll one order for its OTP | 404 if the order isn't yours |
curl -s -H 'X-API-Key: YOUR_API_KEY' 'https://api.pvapins.com/api/v1/services'
# {"services":[{"code":"wa","name":"Whatsapp"},{"code":"course-hero","name":"Course Hero"}, ...]}curl -s -H 'X-API-Key: YOUR_API_KEY' \
'https://api.pvapins.com/api/v1/operators?country=US&service=wa'
# {"operators":[{"operator":2,"label":"Operator 2","price":0.15,"count":42}, ...]}
# count: null means "stock unknown", NOT zero — those operators are still buyable.curl -s -X POST 'https://api.pvapins.com/api/v1/orders' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: YOUR_UNIQUE_ID' \
-d '{"country":"US","service":"wa","operator":2}'
# {"id":"1048576","phoneNumber":"+15551234567","status":"active","price":0.15,"expires_in":1200}
# Omit "operator" to auto-pick the most reliable operator in stock and bill its price.# Poll every 3-5s until otpCode is filled or status leaves "active".
# There are no customer webhooks — polling is the delivery path.
curl -s -H 'X-API-Key: YOUR_API_KEY' 'https://api.pvapins.com/api/v1/orders/1048576'
# {"id":"1048576","status":"completed","otpCode":"418494","phoneNumber":"+15551234567"}Service codes are not all two or three letters. Where we have an sms-activate slug we keep it
(wa, tg, go = Gmail, ts = PayPal) — those you cannot guess.
Everything else gets a slug derived from the name (course-hero,
capital-one-shopping), and a service whose name has no Latin characters gets
svc-<id>. Read the code from GET /api/v1/services, or pass the service
name instead — POST /orders, GET /operators and
GET /numbers all accept a name wherever they accept a code.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | BAD_REQUEST | Body was not valid JSON | Fix the request |
| 401 | — | Missing, invalid or revoked key | Check the header; rotate if leaked |
| 403 | — | Key is missing a required scope | Issue a key with that scope |
| 402 | NO_BALANCE | Not enough balance | Top up, then retry |
| 404 | BAD_SERVICE / BAD_COUNTRY / BAD_OPERATOR | Unknown or inactive | Re-read the inventory endpoints |
| 409 | NO_NUMBERS | Out of stock right now | Retry with backoff, or another operator |
| 409 | IN_FLIGHT | Same Idempotency-Key is still being placed | Wait and re-poll — do not send a new order |
| 429 | RATE_LIMITED | Too many requests | Honour Retry-After |
| 502 | SUPPLIER_ERROR | Upstream failure | Retryable |
| 503 | SUPPLIER_DISABLED | Supplier switched off | Try another operator or country |
Rate limits: per API key, per endpoint, per minute — 60/min on most endpoints, higher on
OTP polling, and lower on the calls that consume stock. Over the limit you get 429 with a
Retry-After header; the limits are tuned at runtime, so read the header rather than hardcoding a number.
One-time verification sessions. See full reference for payloads and errors.
country_idcustomer, country (and app per docs)customer, app, countryoperator — when set, app is the group/combine name and operator is the suffix (see full reference)customer, number, country, appoperator — same pairing rules as Get numberFor repeated OTPs or longer access — keep rent calls separate in your codebase.
Repeat logins, re-verification, or when you may need another code on the same line.
Use rent-specific parameters (e.g. is_rent=1) as in the reference below.
curl -s 'https://api.pvapins.com/user/api/get_number.php?customer=YOUR_API_KEY&app=APP_NAME&country=COUNTRY_NAME&is_rent=1'Combine / operator: Optional &operator=… works with rent requests too — same rules as the temporary flow (app = group name when operator is set).
Treat limits as normal — throttle, backoff, and map errors to clear actions.
Start around 3–6s, then backoff; stop after a max wait; log failures.
Throttle concurrent polls; queue requests under load.
One internal table: message → meaning → action (retry / new number / balance / key).
| Error | Meaning | Action |
|---|---|---|
Customer Not Found | Invalid or missing API key | Verify the key you sent; rotate if leaked |
No Number Found | No inventory for selection | Try another country/app or timing |
Not Possible (limit) | Rate limit | Backoff and retry later |
Pick the right model before you wire up automation.
| Option | Best for | Repeat access | Next step |
|---|---|---|---|
| Temporary | One-off verification or testing | No | Run quickstart → |
| Rent | Multiple OTPs / longer window | Yes | Rent options → |
| Rates | Pricing by country & app | — | Pricing → |
Quick answers for integrators.
Register, log in, and copy your key from the dashboard. Use it server-side only.
New integrations should use REST API v1 (https://api.pvapins.com/api/v1): JSON responses, header authentication, ISO country codes and a published OpenAPI spec. The classic /user/api/*.php endpoints are unchanged and still supported, so existing code keeps working — you do not need to migrate.
No. Poll GET /api/v1/orders/{id} every 3–5 seconds until otpCode is filled or status leaves active.
No — there is no CORS, deliberately. Your API key is a server-side secret; put the calls behind your own backend.
Listing countries/apps is typically public; requesting numbers and fetching SMS requires your key.
Request a number, then poll get_sms with backoff until the code appears or you time out.
Temporary is for a short session; rent keeps the same line for repeated codes over a rental period.
Check the customer parameter matches your API key exactly.
Wait, poll with backoff, then try another route; avoid hammering the API.
operator parameter? +Optional. For combine-style services, pass the operator suffix and set app to the group name shown on the site. Omit operator if you use the full app name as app (default behavior).
Yes — design throttling and exponential backoff into your integration.
Use monitoring, error mapping, and compliance controls; follow platform terms.
Expand each block for parameters, examples, and error messages. Switch tabs for temporary vs rent endpoints.
id: Unique country identifierfull_name: Full country name
country_id: Country ID to get apps for (required)
id: Unique app identifierfull_name: Full app nametimestamp: Last update timestamptrending: Trending status (1 = trending, 0 = not trending)deduct: Cost deduction amount
customer: Your API key (required)country: Country name to check rates for
operator is present, app must be the group/combine name (as on the website), and operator is the suffix for that service. Omit operator to keep classic behavior: app = full application name.
customer: Your API key (required)app: Full application name, or group name when using operatorcountry: Country name (e.g., malaysia, indonesia, philippines)operator: Optional — operator suffix for combine-style servicesnumber: Phone number to reuse (optional, for reuse feature only)&number=PHONE_NUMBER parameter to reuse a previously used number?customer=apikey&app=google&country=malaysia&number=112869xxxcustomer: Your API key (required)number: Phone number to check SMS forcountry: Country nameapp: Full application name, or group name when using operatoroperator: Optional — same pairing as Get number
customer: Your API key (required)number: Phone number to rejectcountry: Country nameapp: Full application name, or group name when using operatoroperator: Optional — same pairing as Get number / Get SMSget_reject.php (alternate URL using n_id) supports the same optional operator parameter.customer: Your API key (required)
customer: Your API key (required)
API endpoints for renting phone numbers for extended periods. These endpoints include the is_rent=1 parameter to distinguish from temporary numbers.
is_rent: Set to 1 to get countries available for rent (required)id: Unique country identifierfull_name: Full country name
country_id: Country ID to get apps for (required)is_rent: Set to 1 to get apps available for rent (required)id: Unique app identifierfull_name: Full app nametimestamp: Last update timestamptrending: Trending status (1 = trending, 0 = not trending)deduct: Cost deduction amount for rental
customer: Your API key (required)app: Application name (e.g., google, whatsapp, telegram)country: Country name (e.g., malaysia, indonesia, philippines)data: Phone number (success) or error message (failure)code: Response code (100 = success, 200 = error)
customer: Your API key (required)
id: Unique rental IDuser_id: User identifierkey_id: Key identifierapp_id: Application IDnumber: Rented phone numberdeduct: Cost deduction amountduration: Rental duration in dayscountry_id: Country identifieris_released: Release status (0 = active, 1 = released)auto_renew: Auto-renewal status (0 = disabled, 1 = enabled)received_message: Message count receivedapi: API identifiertimestamp: Rental start timestampapp_name: Application namecountry_name: Country nametimestamp_readable: Human-readable timestampdays: Days remaining in rentalexpiry: Rental expiry date and time
days field shows remaining rental daysis_released to see if a number is still activecustomer: Your API key (required)country: Country name (e.g., USA, UK, malaysia)app: Application name (e.g., Rent GPay GPlay GVoice)number: Rented phone number to check SMS for (required)
from: Sender of the SMS messagemessage: Content of the SMS messagetimestamp: Date and time when the SMS was received
customer: Your API key (required)app: Application name (e.g., google, whatsapp, telegram)country: Country name (e.g., malaysia, indonesia, philippines)number: Phone number to reject (required)
data: Response message (success or error)code: Response code (100 = success, 200 = error)
customer: Your API key (required)app: Application name (e.g., google, whatsapp, telegram)country: Country name (e.g., malaysia, indonesia, philippines)number: Phone number to renew (required)
data: Response message (success or error)code: Response code (100 = success, 200 = error)
Base URL https://api.pvapins.com. Authenticate with
X-API-Key: YOUR_API_KEY (or Authorization: Bearer YOUR_API_KEY).
The same key works here and on the classic endpoints above. Machine-readable spec:
OpenAPI 3.1 · runnable
interactive reference.
balance: spendable balance — your total minus anything held by pending orders. This is the figure ordering is gated on.created_at: snake_case here, camelCase on the order endpoints. Kept that way for compatibility.
code is ISO 3166-1 alpha-2 and is what you pass as country everywhere else. The classic endpoints use country names instead — the two are not interchangeable.wa, tg, acd). These are not derivable from the name — go is Gmail, ts is PayPal, lf is TikTok.-, capped at 48 characters — 5ka.ru → 5ka-ru, Capital One Shopping → capital-one-shopping. Collisions get a numeric suffix (-2, -3).svc-<id>. Address those by name — POST /orders, GET /operators and GET /numbers accept a name wherever they accept a code.
country and service are each optional, but at least one is required:country: ISO-2 country codeservice: service code from /api/v1/services, or the service namepage: bulk listings only — 1-based, default 1limit: bulk listings only — default 500, maximum 2000. Out-of-range values are clamped, never rejected.
service only — every country that sells it. Each row carries country.country only — every service available there. Each row carries service.422. There is no all-routes mode: we publish roughly 6,900 services across 216 countries, so page through one of the two bulk modes instead.
count is always null in the two bulk modes. Live stock costs us one supplier call per operator, so we do not fetch it for a listing thousands of rows long. As everywhere in this API, null means stock unknown — it never means zero or sold out. Ask for the single route you are about to buy if you need a live figure. Bulk rows also carry no label.
operator: the public displayId you pin on POST /ordersprice: per-OTP price in USD — the price we quote is the price we chargecount: numbers in stock. null means unknown, not zero — some suppliers expose no stock figure and those operators are still buyable.
country (or service) and then operator ascending instead — a ranking across thousands of unrelated routes would mean nothing, and a listing that size needs to be predictably addressable so paging is repeatable.operator: 1 under wa/US and operator: 1 under tg/GB are different operators. Never key a cache or a config on the number alone.
404 — unknown country or service422 — neither country nor service supplied; at least one is required. Note this one error uses a nested shape: {"error":{"code":"VALIDATION_ERROR","message":"…"}}country: ISO-2 country code (optional)service: service code or name (optional)price is non-null only when both are supplied, and it is the auto-pick price — exactly what POST /orders bills when you omit operator.
country: ISO-2 country code, case-insensitive (required)service: service code or name (required)operator: optional displayId from /api/v1/operators. Omit it and we auto-pick the most reliable operator in stock and bill that price.Idempotency-Key — optional, but recommended. A retry with the same key returns the original order for 30 minutes instead of buying a second number.
status: one of pending, active, completed, cancelled, expiredexpires_in: seconds until the order expires — always 1200 (20 minutes)price: what you were charged, in USD
{"error":"CODE","message":"…"}:
400 BAD_REQUEST — body was not valid JSON402 NO_BALANCE — insufficient balance404 BAD_SERVICE / BAD_COUNTRY / BAD_OPERATOR — unknown or inactive409 NO_NUMBERS — out of stock; retryable with backoff409 IN_FLIGHT — an order with the same Idempotency-Key is still being placed. Wait and re-poll; do not send a new order.429 RATE_LIMITED — honour the Retry-After header502 SUPPLIER_ERROR — upstream failure; retryable503 SUPPLIER_DISABLED — supplier switched offotpCode is populated or status leaves active, then stop.message carries the OTP code, not the full SMS body — our suppliers do not reliably return the full text.
404 — no such order, or the order is not yours