REST API — handler_api Compatible

SMS Verification API Documentation

Connect Turonay to your own software, bot or panel. With a single API key you can get a number, read the incoming SMS code and cancel the activation. The API is compatible with the handler_api protocol: if you already have an integration written for it, you usually only need to change the base URL and key. Instead of polling for the code you can also receive a signed webhook notification.

11
Endpoints — 5 minutes to learn
GET
One method, plain-text + JSON responses
Refund
No code, no charge

Quick Start

You can rent your first number in three steps. Open the panel and create your API key, then run the request below.

# 1) Check your balance curl "https://turonay.com/api/?api_key=YOUR_KEY&action=getBalance" # → ACCESS_BALANCE:125.4000 # 2) Rent a number (service code + country id) curl "https://turonay.com/api/?api_key=YOUR_KEY&action=getNumber&service=wa&country=62" # → ACCESS_NUMBER:184213:628123456789 # 3) Poll for the code (every few seconds) curl "https://turonay.com/api/?api_key=YOUR_KEY&action=getStatus&id=184213" # → STATUS_WAIT_CODE … then → STATUS_OK:123456

Classic endpoints return plain text (colon-separated fields, handler_api format); V2 and discovery endpoints return JSON. Instead of polling for the code you can use webhooks.

Base URL and Authentication

All requests go to one of the two equivalent addresses below with GET; both return the same response:

https://turonay.com/api/ https://turonay.com/stubs/handler_api.php

Coming from another provider that uses the handler_api protocol? In most integrations changing the base URL and API key is enough: the /stubs/handler_api.php path follows the widely used handler_api.php convention; parameters and response formats follow the same protocol, with a few known differences.

Authentication uses the api_key query parameter. No headers are involved. If you do not want the key in URLs or server logs, you can also send the parameters as POST form fields (application/x-www-form-urlencoded). You can regenerate the key from the panel at any time; the old key stops working immediately.

API access requires a total top-up of at least 100,00 ₺ and a verified email address. If a requirement is not met, every request returns an error starting with API_ACCESS_DENIED (see the error table below).

Never use your key client-side. A key embedded in browser code lets anyone spend your balance — make the calls from your own server.

Rate Limit

The API is rate limited per key and per IP, per minute. Requests over the limit return HTTP 429 with NO_CONNECTION in the body.

Do not poll once per second while waiting for a code. The recommended interval is 3–5 seconds; when you receive a 429, back off by doubling the wait (e.g. 5s, 10s, 20s).

The default limit is 60 requests per minute per key and IP. To stay under it, use webhooks instead of frequent polling.

Billing

You are charged only when the SMS code reaches you. When you get a number the price is reserved on your balance (getBalance shows the lower available amount); the fee is only finalised when the code is delivered. If you cancel before a code arrives, or the number expires, you pay nothing.

Your balance must cover the price when you request a number, otherwise NO_BALANCE is returned. Once the code has arrived the activation can no longer be cancelled (setStatus&status=8): EARLY_CANCEL_DENIED is returned and nothing is refunded because the service was delivered. If the provider cancels the number, or it expires before a code arrives, the reserved amount is released back to your balance.

Endpoints

actionParametersSuccess responseDescription
getPricesservice, country (optional){"62":{"wa":{"cost":12.50,"count":744716}}}Price and stock for sellable service/country pairs. cost is the amount that will be deducted from your balance, with your tier discount already applied. The quoted price is locked briefly: even if the upstream server changes, you are charged what you saw.
getCountries—{"62":{"id":62,"eng":"Turkey"}}Countries you can actually buy numbers for. Only countries with real stock are listed.
getNumbersStatuscountry{"wa_0":"744716"}Stock per service for one country. Key format is service_0.
getBalance—ACCESS_BALANCE:<amount>Returns your available balance.
getOperatorscountry (optional){"status":"success","countryOperators":{"62":["any"]}}Selectable operators per country. Pass one of these values as the operator parameter of getNumber / getNumberV2.
getNumberservice, country, operator (optional), maxPrice (optional)ACCESS_NUMBER:<id>:<phone>Allocates a number. The returned id is used in later calls. If you send maxPrice, the number is not taken when the price exceeds it.
getNumberV2service, country, operator (optional), maxPrice (optional){"activationId":"184213","phoneNumber":"628123456789",…}Same as getNumber, but the response is JSON and also includes price, expiry and operator.
getStatusidSTATUS_WAIT_CODE / STATUS_WAIT_RETRY:<code> / STATUS_OK:<code> / STATUS_CANCELAsks whether the SMS code has arrived. The fee is finalised at the moment the code arrives.
getStatusV2id{"sms":{"code":"123456",…}}JSON counterpart of getStatus. When the code has arrived it returns JSON with an sms object; while no code exists it returns the plain-text status (STATUS_WAIT_CODE etc.), not JSON.
getActiveActivations—{"status":"success","activeActivations":[…]}List of the open (not expired / not closed) activations on your account. Use it to resume after a lost connection or restart.
setStatusid, status (1 / 3 / 6 / 8)ACCESS_READY / ACCESS_RETRY_GET / ACCESS_ACTIVATION / ACCESS_CANCEL1 ready, 3 request another SMS (free), 6 complete (only after a code has arrived), 8 cancel (denied once a code has arrived).

getNumber — Renting a Number

service is the short service code (e.g. wa for WhatsApp) and country is the country id. Current codes and ids are listed on the pricing page.

Optional: operator requests a specific operator (getOperators); maxPrice is the highest price you accept — if the current price is higher, no number is taken.

GET ?action=getNumber&service=wa&country=62&maxPrice=15 ACCESS_NUMBER:184213:628123456789 │ └─ phone └─ order id NO_NUMBERS → no stock right now NO_BALANCE → insufficient balance BAD_SERVICE → unknown service code / not for sale WRONG_MAX_PRICE:18.40 → maxPrice is invalid or does not cover the current price; nothing is purchased BAD_KEY → invalid API key

When you get a number the price is reserved on your balance (getBalance shows the lower available amount); the fee is finalised only when the SMS code arrives. If the activation is cancelled or expires without a code, the reserved amount is released. Your balance must cover the price.

getStatus — Reading the Code

The id value comes from the getNumber call.

GET ?action=getStatus&id=184213 STATUS_WAIT_CODE → code has not arrived yet STATUS_OK:123456 → code received STATUS_WAIT_RETRY:123456 → you requested another SMS (setStatus&status=3) and the previous code is still shown; it turns into STATUS_OK when the new code arrives STATUS_CANCEL → cancelled / expired

To be notified when the code arrives without polling, use Webhooks (typically delivered within 1–2 minutes). After you receive the code, we recommend closing the activation with setStatus&status=6.

getNumberV2 — Get a Number as JSON

Takes the same parameters as getNumber and returns JSON on success. On failure the classic plain-text codes (NO_NUMBERS, NO_BALANCE, WRONG_MAX_PRICE:<price> ...) are returned.

GET ?action=getNumberV2&service=wa&country=62 { "activationId": "184213", "phoneNumber": "628123456789", "activationCost": "12.50", "currency": 949, "countryCode": "62", "canGetAnotherSms": true, "activationTime": "2026-10-07 12:00:00", "activationEndTime": "2026-10-07 12:20:00", "activationOperator": "any" }
fieldDescription
activationIdActivation id; used as id in getStatus / setStatus calls.
phoneNumberPhone number (with country code, no + sign).
activationCostPrice (as a string, e.g. "12.50"); the amount that is finalised if the code arrives.
currencyCurrency: ISO-4217 numeric code (integer; e.g. 949 = TRY).
countryCodeCountry id.
canGetAnotherSmsWhether another SMS can be requested on the same number (setStatus&status=3).
activationTimeWhen the activation started (server time, Europe/Istanbul, UTC+3; Y-m-d H:i:s format, no time zone).
activationEndTimeWhen the number's validity ends; the activation is cancelled after this if no code arrived. Same format and time zone as activationTime.
activationOperatorOperator used (any = no preference).

getStatusV2 — Status as JSON

The id value comes from the getNumber call. Once the code has arrived it returns JSON: the sms object contains dateTime (ISO-8601), code and text; call is currently always null. While there is no code yet, the response is the plain-text status, not JSON (STATUS_WAIT_CODE, STATUS_WAIT_RETRY:..., STATUS_CANCEL).

GET ?action=getStatusV2&id=184213 // when a code has arrived: JSON { "verificationType": 0, "sms": { "dateTime": "2026-10-07T12:00:41+03:00", "code": "123456", "text": "Your code: 123456" }, "call": null } // while no code exists: plain-text status, not JSON STATUS_WAIT_CODE

getActiveActivations — Open Activations

Lists the open activations on your account as JSON (id, service, number, amount, status and any received codes). activationStatus: "4" waiting for the code, "1" code received, "3" waiting for a resent SMS. The smsCode array is the authoritative signal that a code has arrived; do not rely on the status field alone. Use it to pick up pending work when your application restarts.

GET ?action=getActiveActivations { "status": "success", "activeActivations": [ { "activationId": "184213", "serviceCode": "wa", "phoneNumber": "628123456789", "activationCost": "12.50", "activationStatus": "1", "smsCode": ["123456"], "countryCode": "62" } ] }

setStatus — Change Status

Example: GET ?action=setStatus&id=184213&status=6. Four statuses are accepted:

statusSuccess responseDescription
1ACCESS_READYNumber ready / waiting for SMS (informational).
3ACCESS_RETRY_GETRequest another SMS on the same number. It is free; if a new code arrives it moves between STATUS_WAIT_RETRY and STATUS_OK.
6ACCESS_ACTIVATIONComplete and close. Valid only once a code has arrived; without a code it returns STATUS_WAIT_CODE (or BAD_STATUS) and the activation is not completed.
8ACCESS_CANCELCancel the activation. If no code has arrived it is cancelled and the reserved amount is released. If a code has arrived, EARLY_CANCEL_DENIED is returned; the fee is final and not refunded.

Error Codes

CodeMeaningWhat to do
BAD_KEYAPI key is empty or invalid.Check the key in the panel; if you regenerated it, update your code.
BAD_ACTIONUnknown action value.Only the action values in this document are valid.
BAD_SERVICEService/country missing or not on sale.Verify the parameters and the status in the price list.
NO_BALANCEInsufficient balance or below the minimum.Top up your balance and try again.
NO_NUMBERSNo stock for that service/country right now.Try a different country or retry shortly.
NO_ACTIVATIONThe given id is not yours.Make sure the id came from the getNumber response.
BAD_STATUSInvalid status for setStatus.Only 1, 3, 6 and 8 are accepted.
NO_CONNECTIONTemporary issue or rate limit (HTTP 429).Wait and retry, backing off progressively.
WRONG_MAX_PRICE:<price>The maxPrice you sent is invalid or does not cover the current price; nothing is purchased.If you accept the price in the response, send it as maxPrice, or repeat the request without maxPrice.
EARLY_CANCEL_DENIEDThe activation cannot be cancelled (setStatus&status=8) because the code has already arrived.The service was delivered; the fee is not refunded. Close the activation with status=6.
API_ACCESS_DENIED:MIN_DEPOSIT:<amount>The key is valid, but your account's total top-up is below the amount required for API access (the TRY amount in the response).Top up your balance in the panel; requests work as soon as your total top-up reaches this amount. You do not need to rotate the key.
API_ACCESS_DENIED:EMAIL_NOT_VERIFIEDThe key is valid, but your account's email address is not verified.Request the verification email from your profile page and click the link; requests work right after verification.

PHP Example

<?php $key = 'YOUR_KEY'; $base = 'https://turonay.com/api/'; function tur(string $base, array $q): string { return trim((string)file_get_contents( $base . '?' . http_build_query($q) )); } // Rent a number $r = tur($base, ['api_key'=>$key, 'action'=>'getNumber', 'service'=>'wa', 'country'=>'62']); if (!str_starts_with($r, 'ACCESS_NUMBER')) { exit($r); } [, $id, $phone] = explode(':', $r); // Wait for the code for ($i = 0; $i < 40; $i++) { sleep(4); $s = tur($base, ['api_key'=>$key, 'action'=>'getStatus', 'id'=>$id]); if (str_starts_with($s, 'STATUS_OK')) { echo explode(':', $s)[1]; break; } if ($s === 'STATUS_CANCEL') { break; } }

Python Example

import time, requests KEY = "YOUR_KEY" BASE = "https://turonay.com/api/" def tur(**q): q["api_key"] = KEY return requests.get(BASE, params=q, timeout=20).text.strip() # Rent a number r = tur(action="getNumber", service="wa", country="62") if not r.startswith("ACCESS_NUMBER"): raise SystemExit(r) _, oid, phone = r.split(":") print("number:", phone) # Wait for the code for _ in range(40): time.sleep(4) s = tur(action="getStatus", id=oid) if s.startswith("STATUS_OK"): print("code:", s.split(":")[1]); break if s == "STATUS_CANCEL": break

Webhooks (Notifications)

When a code arrives or an activation is cancelled we send a signed HTTPS POST to your server, so you do not need to keep polling getStatus. You set the URL and the secret from your profile page; you can send a test event from there and see the last 10 deliveries.

eventDescription
code_receivedAn SMS code arrived for the activation (the fee is finalised at this moment). For additional SMS (status=3) each new code arrives as a separate event.
cancelledThe activation was cancelled by the provider before a code arrived, or you cancelled it with setStatus&status=8; the reserved amount is released and nothing was charged. No notification is sent for a silent expiry: for activations that run out of time, watch activationEndTime or query getStatus.
testSent when you press "Send test" in your profile; contains sample values.

URL rules: https:// only, port 443 or 8443, at most 500 characters, no username/password, and a publicly reachable domain name (IP addresses, localhost and private-network addresses are rejected). Redirects (3xx) are not followed; a valid TLS certificate is required.

Request Body

The body is JSON. For the cancelled event the code and text fields are null.

POST https://your-server.example.com/turonay-webhook Content-Type: application/json { "event": "code_received", "activationId": "184213", "phone": "628123456789", "service": "wa", "country": "62", "code": "123456", "text": "Your code: 123456", "receivedAt": "2026-10-07T12:00:41Z", "sentAt": "2026-10-07T12:00:42Z", "id": "dlv_9f2c4a7d1b3e5f60a8c2d4e6" }

code is set only for code_received; text is the SMS text from the operator (may be empty). id is unique per delivery and stays the same across retries: use it for de-duplication.

Headers

headerDescription
Content-Typeapplication/json
User-AgentTuronay-Webhook/1.0
X-Turonay-EventEvent name (code_received, cancelled, test).
X-Turonay-TimestampSend time, Unix seconds. Part of the signature.
X-Turonay-Signaturesha256= + hex HMAC-SHA256 signature (see below).
X-Turonay-DeliveryDelivery id (same as id in the body).

Verifying the Signature

To verify that a request came from us, compute HMAC_SHA256(secret, timestamp + "." + rawBody) and compare it with the value in the X-Turonay-Signature header (which starts with sha256=) using a constant-time comparison. The secret (whsec_...) is generated on your profile page and shown in full only at the moment it is generated.

Use the raw body (the bytes you received); do not parse and re-serialise the JSON. Reject the request if the timestamp is older than 5 minutes (replay protection). If you think the secret leaked, rotate it from your profile; the old one stops working immediately.

Verifying in PHP

<?php $secret = 'whsec_...'; // the secret from your profile $raw = file_get_contents('php://input'); // RAW body, not parsed JSON $ts = $_SERVER['HTTP_X_TURONAY_TIMESTAMP'] ?? ''; $sig = $_SERVER['HTTP_X_TURONAY_SIGNATURE'] ?? ''; // reject old requests (5 min) if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit; } $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit; } $e = json_decode($raw, true); if ($e['event'] === 'code_received') { // $e['activationId'], $e['code'] ... } http_response_code(200); // return 2xx within 5 s

Verifying in Node.js

const crypto = require('crypto'); const express = require('express'); const app = express(); const SECRET = process.env.TURONAY_WEBHOOK_SECRET; // RAW body, not parsed JSON app.post('/turonay-webhook', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.get('X-Turonay-Timestamp') || ''; const sig = req.get('X-Turonay-Signature') || ''; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const expected = 'sha256=' + crypto.createHmac('sha256', SECRET) .update(ts + '.').update(req.body).digest('hex'); const a = Buffer.from(expected), b = Buffer.from(sig); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401); const e = JSON.parse(req.body.toString('utf8')); if (e.event === 'code_received') { /* e.activationId, e.code */ } res.sendStatus(200); // return 2xx within 5 s });

Verifying in Python

import hmac, hashlib, time from flask import Flask, request, abort app = Flask(__name__) SECRET = b"whsec_..." # the secret from your profile @app.post("/turonay-webhook") def hook(): raw = request.get_data() # RAW body, not parsed JSON ts = request.headers.get("X-Turonay-Timestamp", "") sig = request.headers.get("X-Turonay-Signature", "") if not ts.isdigit() or abs(time.time() - int(ts)) > 300: abort(400) expected = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): abort(401) e = request.get_json(force=True) if e["event"] == "code_received": print(e["activationId"], e["code"]) return "", 200 # return 2xx within 5 s

Retry Policy

Your server must answer within 5 seconds with a 2xx status. Any other status, a timeout or a connection error counts as a failure. The response body is not read.

A failed delivery is attempted up to 8 times; the waits are 1, 2, 4, 8, 16, 32 and 60 minutes (about 2 hours in total). If all attempts fail the event is dropped.

The same event can arrive again on a retry (same id); make your handling idempotent by activationId. Event order is not guaranteed.

A webhook is a convenience, not your only safety net: if you miss an event you can always query the state with getStatus.

Frequently Asked Questions

What developers ask most about the integration.

Support
Is the API paid?

There is no separate fee or subscription for API access. You are only charged for the numbers you rent, and transactions where no code arrives are not charged at all.

Will my existing integration written for the handler_api protocol work?

The response format is compatible. In most cases you only need to change the base URL and the API key. You should check the price list for service codes and country ids.

Are responses JSON or plain text?

Classic endpoints (getNumber, getStatus, setStatus, getBalance) return plain text for handler_api compatibility; getNumberV2, getStatusV2, getActiveActivations, getOperators and the price/country endpoints return JSON.

What happens if I regenerate my key?

The old key is invalidated instantly and every request using it returns BAD_KEY. Be ready to update your application before regenerating.

How many numbers can I rent at once?

The number of simultaneous activations depends on your balance and the per-minute request limit (default 60 requests per minute per key and IP).

Am I charged if the code never arrives?

No. You are charged only when the SMS code reaches you. If you cancel before a code arrives, or the number expires, nothing is charged.

Should I poll for the code or use webhooks?

They work together. The webhook notification is queued as soon as the code is recorded and is typically delivered within 1–2 minutes; getStatus remains an alternative path and the safety net for missed events and restarts. Most integrations use the webhook as the main path and polling as the backup.

Service Codes

Short service codes, country ids and current pricing.

Integrations

Ready-made integration options and supported platforms.

Technical Support

Write to us wherever you get stuck during integration.