A REST API for buying virtual numbers: codes by SMS and by call, number rentals for a term, the service and country catalog, statuses and webhooks. The balance is shared with the Telegram bot and the web account.
Call the API from your server. The key can spend your balance, so never pass it to a browser or a mobile app.
Getting started
Quick start
Six steps from an API key to an SMS code. Every example runs in a terminal.
1Get an API key
Open @HushNumBot, go to Information → API and press Generate. In the web account the key is under Settings → API and webhook.
The key already starts with hsk_. Paste it whole, without adding anything.
The full key is shown only once, so save it right away.
An account has one active key. A new key turns off the previous one.
Terminal
# Save the key in an environment variable
export HUSH_API_KEY="PASTE_THE_KEY_FROM_THE_BOT"
2Check the key
The profile request buys and changes nothing. If it returns 200, the key works, and the response shows your balance.
A 401 missing_token error means the header did not arrive; here 403 forbidden means the key is invalid. On a purchase, 403 can also mean the account is restricted. More in Authentication.
Find out which countries have numbers for your service and what they cost. The service code is in the Services catalog or in GET /v1/services.
By default countries are sorted by price (sort=price). The quantity field shows how many numbers are available right now. Country names in API responses are in Russian; the Countries catalog has the English names.
The Idempotency-Key header makes sure a repeated purchase request never charges you twice.
Why it matters
Networks are unreliable: the request may have arrived and bought a number while the response got lost. If you simply send the purchase again, you get a second number. With an idempotency key, the repeat returns the same order.
Rules
The header is required for POST /v1/numbers. Without it you get 400 missing_idempotency_key.
The key is a string of 1 to 128 characters. A UUID is the easiest choice.
One key per purchase. Create a new key for every new number.
A repeat with the same key and the same service and country returns the saved result.
A finished result is kept for 24 hours. An unfinished purchase stays protected until its result is known.
A block on new purchases does not hide the result of a request already started with the same key. A purchase already let through may still finish.
When to repeat a request
Response
What to do
403forbidden
Do not repeat automatically. Check the key or ask support about the account restriction.
409idempotency_key_processing
The purchase is still running. Repeat in a few seconds with the same key.
503purchase_pending
We are checking the purchase result. Repeat with the same key. If the wait drags on, contact support.
503balance_busy
The purchase did not start. Wait and make a new attempt with a new key.
503upstream_error on a purchase
The service is busy and the purchase was declined. Wait and make a new attempt with a new key.
504upstream_timeout
The service did not answer in time. Repeating with the same key is safe.
A network error or a client timeout
Repeat with the same key.
409idempotency_key_reused_with_different_payload
The key was already used for another service/country pair. Create a new key.
If the response has Retry-After, wait that many seconds before repeating. For temporary failures it is usually 3 seconds now. The header sets only the delay: whether to keep the key or take a new one depends on the error code.
After a network drop or an unconfirmed purchase, do not create a new key. Keep the old one, even if you stopped the script, and use it on the next run.
With a valid API key, a block on new purchases does not stop you from reading your orders and cancelling them under the usual rules. Maintenance restrictions still apply. If a request was declined before the purchase started, you can use the same unused key once the block is lifted.
Price changes
If the provider price rose during the purchase, the order is still created at the final price, and the response includes price_changed: true and previous_price (the catalog price before the recalculation).
The number is issued, no SMS yet. You can wait or cancel.
sms_received
At least one SMS arrived; codes are in sms_codes.
finished
Work with the order is done.
canceled
The order is cancelled and the number released.
refunded
The money for the order is back on the balance.
Methods step by step
Step
Method
Why
Catalog
GET/v1/services
Services with stock and the lowest price. sort=popularity or name.
Countries
GET/v1/services/{code}/countries
Countries for the service and the best price. sort=price or name.
Offers
GET/v1/services/{code}/offers?country=0
All prices and stock for the service in a country.
Purchase
POST/v1/numbers
Create an order. Needs Idempotency-Key.
Status
GET/v1/numbers/{order_id}
The whole order, including sms_codes.
Codes
GET/v1/orders/{order_id}/sms
Only the status and the list of codes.
Active
GET/v1/numbers
Orders in pending and sms_received, up to 200.
Finish
POST/v1/orders/{order_id}/finish
Close the order after the SMS arrived.
Cancel
POST/v1/numbers/{order_id}/cancel
Cancel an order without an SMS and get the money back.
History
GET/v1/orders/history
Past activations with a period filter.
Cancelling an order
You can cancel only your own order in the pending status that has not received an SMS yet.
Right after the purchase, cancelling is unavailable for a few seconds, and with some providers for up to 2 minutes. details.seconds_left in the 409 too_early response tells how long is left.
After the code arrives, cancelling is unavailable (409 already_has_code): finish the order instead.
An order in another status returns 409 bad_status.
Finishing
POST /v1/orders/{order_id}/finish answers 204 with no body. Before the SMS arrives or in an unsuitable status it returns 409 cannot_complete.
Activation history
Filter by period with start and end (Unix time). For long histories use the cursor: pass next_after_id from the response as the after_id parameter until has_more becomes false. The history keeps a stable order by time and order ID; size sets the page size, up to 100.
POST /v1/numbers/{order_id}/cancel
curl -X POST https://api.hush-sms.com/v1/numbers/66f2b8a8e7ad0a0012c34567/cancel \
-H "Authorization: Bearer $HUSH_API_KEY"
{
"error": {
"code": "too_early",
"message": "Cancel not allowed until 120s after order creation for this provider",
"details": { "seconds_left": 45 }
}
}
Full example
A Python script buys a number, waits up to 5 minutes for the code and finishes the order, or cancels it if no SMS arrived.
On a network drop, 504, idempotency_key_processing and purchase_pending, it repeats with the same key.
On 503 balance_busy or 503 upstream_error the purchase was declined. The next attempt goes with a new key after a pause.
The script follows Retry-After. If it runs out of attempts, the error message includes the last key: keep it to check the result.
Other errors are raised as exceptions: handle them by the error table.
Requires Python 3.10+ and the requests library.
buy_number.py
import os
import time
import uuid
import requests
API = "https://api.hush-sms.com"
HEADERS = {"Authorization": f"Bearer {os.environ['HUSH_API_KEY']}"}
def error_code(resp):
try:
return resp.json()["error"]["code"]
except (ValueError, KeyError, TypeError):
return None
def retry_delay(resp) -> int:
try:
return max(1, int(resp.headers.get("Retry-After", "3")))
except ValueError:
return 3
def buy_number(service: str, country: int, *, idempotency_key: str | None = None) -> dict:
key = idempotency_key or str(uuid.uuid4())
for attempt in range(3):
try:
resp = requests.post(
f"{API}/v1/numbers",
headers={**HEADERS, "Idempotency-Key": key},
json={"service": service, "country": country},
timeout=(5, 60),
)
except (requests.Timeout, requests.ConnectionError) as error:
if attempt == 2:
raise RuntimeError(
f"No response. Repeat with the same Idempotency-Key: {key}"
) from error
time.sleep(3)
continue
code = error_code(resp)
new_attempt = resp.status_code == 503 and code in {"balance_busy", "upstream_error"}
retry = new_attempt or resp.status_code in {429, 504} or code in {
"idempotency_key_processing", "purchase_pending"
}
if retry and attempt < 2:
time.sleep(retry_delay(resp))
if new_attempt:
key = str(uuid.uuid4())
continue
if resp.status_code >= 400:
raise requests.HTTPError(
f"HTTP {resp.status_code}, code={code}; Idempotency-Key: {key}",
response=resp,
)
return resp.json()["order"]
def wait_for_code(order_id: str, timeout_sec: int = 300) -> str | None:
deadline = time.monotonic() + timeout_sec
while time.monotonic() < deadline:
resp = requests.get(f"{API}/v1/numbers/{order_id}", headers=HEADERS, timeout=15)
resp.raise_for_status()
order = resp.json()["order"]
if order.get("sms_codes"):
return order["sms_codes"][-1]
if order["status"] != "pending":
return None
time.sleep(4)
return None
order = buy_number("tg", 0)
print("Number:", order["phone_number"])
code = wait_for_code(order["id"])
if code:
print("Code:", code)
requests.post(f"{API}/v1/orders/{order['id']}/finish", headers=HEADERS, timeout=15)
else:
requests.post(f"{API}/v1/numbers/{order['id']}/cancel", headers=HEADERS, timeout=15)
Guides
Code by call
Some services confirm a number with an incoming call instead of an SMS. Such a number is bought with the same POST /v1/numbers method and the field verification_type: "call".
How to buy
Find the service: GET /v1/services?include_calls=1 adds verification_types to each service. If the list has call, the code can come by call.
Choose a country: GET /v1/services/{code}/countries?verification_type=call returns countries and prices for calls. They can differ from SMS prices.
Buy the number with "verification_type": "call" and the required Idempotency-Key.
Wait for the call: poll GET /v1/numbers/{order_id} or GET /v1/orders/{order_id}/sms; the answer appears in call_confirmations.
verification_type is part of the purchase body for idempotency: the same key for an SMS and for a call returns 409 idempotency_key_reused_with_different_payload.
caller_id: the full caller number. The service asks for its last digits, usually 4, 5 or 6: the service screen says how many.
Cancelling, finishing and statuses work as for an SMS order. How long the number waits for a call depends on the issued number and can differ from 15 minutes. If no confirmation arrives, the refund happens after the number cancellation is confirmed.
A number for a term from a few hours to a few months. For the whole term it receives SMS from the chosen service, and with a full rental (full_rent) from any service. Good for repeat sign-ins and account recovery.
Methods step by step
Step
Method
Why
Services
GET/v1/rent/services
What can be rented and in how many countries. Rental codes are their own, for example telegram or full_rent.
Countries
GET/v1/rent/services/{code}/countries
Countries, available terms in hours and the price of the shortest term.
Extension terms with prices, and the extension itself. Needs Idempotency-Key.
Cancel
POST/v1/rentals/{rental_id}/cancel
Cancel with a refund while it is available.
Terms are given in hours: 24 is a day, 168 a week, 720 a month. Prices are in rubles, as everywhere in the API.
Renting without price surprises
Take the term price from GET /v1/rent/services/{code}/countries/{country} and pass it as max_price. If the price has risen, the rental is not created, no money is charged, and the 409 price_changed response shows the new price in details.price. Confirm it with a new request and a new Idempotency-Key.
If the request was cut off or returned 503 purchase_pending, repeat it with the same key: a second number will not be rented.
Without max_price the rental goes through at the current term price.
{
"error": {
"code": "price_changed",
"message": "The rental price has changed; confirm the new price with a new Idempotency-Key",
"details": { "price": 305.0 }
}
}
Extension and cancellation
The actions field shows what you can do with the rental right now:
extend: the rental can be extended. GET /v1/rentals/{rental_id}/extension returns terms with prices, and POST /v1/rentals/{rental_id}/extend extends for the chosen term. The term is added to the current one; the number and SMS stay.
cancel: the rental can be cancelled with a full refund until cancel_until, if the number has not received an SMS yet. While cancel_after is present, cancelling opens at that time.
An extension is idempotent, like a rental: a repeat with the same Idempotency-Key does not charge twice. max_price protects against a price rise just as for a rental.
Some numbers can be extended and others cancelled: it depends on the number. Always check actions.
{
"error": {
"code": "too_early",
"message": "Cancel is available a little later",
"details": { "seconds_left": 37 }
}
}
Rental SMS
Poll GET /v1/rentals/{rental_id} no more than once every 5 seconds: new SMS are added at the end of messages. To skip polling, set up a webhook: API rentals send the rental.sms.received event.
Statuses
active: the rental is running and SMS arrive.
finished: the term is over.
canceled: the rental is cancelled.
Where else the rental shows up
API rentals show up in the Telegram bot under «My rentals» and in the web account on the website; the balance is shared.
The code from the SMS: the first number of 4–8 digits. If there is no number, the whole text.
verificationType
string
Only for call orders: call. Regular SMS events do not have this field.
phoneFrom
string
The caller number, if the confirmation came by a call without a separate code. In that case code: null.
test
boolean
true for a test delivery from the bot.
For a number with a code by call (from the bot, the website or verification_type: "call" in the API), the webhook carries the code in code or the full caller number in phoneFrom. The service where you enter the number decides how many last digits it needs. The X-Hush-Event: sms.received header stays for compatibility.
In the API such an order has verification_type: "call" and call_confirmations: kind: "code" means a ready code, kind: "caller_id" means the full caller number in the value field. Regular SMS responses do not change.
Rental SMS
For rentals created with POST /v1/rentals, each new SMS comes as a separate event with the X-Hush-Event: rental.sms.received header. Rentals from the bot and the website do not send these events, so your activation handler never gets unfamiliar data.
rentalId: the rental ID, as in GET /v1/rentals/{rental_id}.
phoneNumber: the rented number.
code: the first number of 4–8 digits in the text, or null.
Answer with any 2xx code within 12 seconds and the event counts as delivered. Do heavy processing after the response, in the background. Redirects are not followed.
When we retry delivery
A network error or a timeout, responses 408, 425, 429 and any 5xx.
Up to 8 attempts with a growing pause: 5 s, 10 s, 20 s, 40 s and so on.
Other 4xx codes are not retried: the event counts as rejected.
One event can arrive twice if your server processed it but did not manage to answer. Handle events idempotently, for example by the activationId + code pair, and for a call without a code use phoneFrom.
A webhook adds to the API, it does not replace it: codes are always available in GET /v1/numbers/{order_id}.
webhook.py · Flask
from flask import Flask, request
app = Flask(__name__)
seen = set() # in production: a database table
@app.post("/hooks/hush")
def hush_webhook():
if request.headers.get("X-Hush-Event") != "sms.received":
return "", 204
event = request.get_json(force=True)
if event.get("test"):
return "", 204
confirmation = event.get("code") or event.get("phoneFrom")
key = (event["activationId"], confirmation)
if key not in seen:
seen.add(key)
save_code(event["activationId"], confirmation, event.get("text"))
return "", 204
Guides
Errors and limits
All errors come in one format. Rely on the code field: the message text may change.
Error format
code: a stable machine code; build your logic on it.
message: a description for logs.
details: extra data, if any. For example, seconds_left for too_early.
Invalid JSON, missing service or country, wrong parameters.
Check the request body and parameters.
400
missing_idempotency_key
A purchase without the Idempotency-Key header.
Add the header with a unique key.
401
missing_token
No Authorization header.
Pass Bearer with the key.
402
insufficient_funds
Not enough money on the balance.
Top up the balance in the bot or on the website.
403
forbidden
The key is invalid, or new purchases are blocked for the account.
Check the key. If it is valid, contact support; do not repeat automatically.
404
not_available
No free numbers for the service in the country.
Choose another country or try later.
404
not_found
The order was not found or belongs to another account.
Check the order_id.
409
idempotency_key_processing
A purchase with this key is still running.
Repeat in a few seconds with the same key.
409
idempotency_key_reused_with_different_payload
The key was already used with another request body: service, country, verification_type or hours.
Create a new key.
409
price_changed
The rental price rose above max_price. The rental was not created and no money was charged.
Check the new price in details.price and repeat with a new key.
409
too_early
A cancellation before the allowed time.
Wait details.seconds_left seconds.
409
bad_status
Cancelling an order that is not in the pending status.
Check the order status.
409
already_has_code
Cancelling after the SMS arrived.
Finish the order with finish.
409
cannot_complete
Finishing before the SMS arrived or in an unsuitable status.
Wait for the SMS or cancel the order.
409
order_locked
The order is being processed right now.
Repeat in a couple of seconds.
429
no code
The request rate limit is exceeded. The response body is not JSON.
Pause and lower the request rate.
500
internal_error
A temporary internal error.
Try again later.
502
provider_error, upstream_error
The provider or an internal service did not complete the operation.
Try again later; repeat a purchase with the same key.
503
maintenance, service_unavailable
Maintenance or temporary unavailability.
Try again later.
503
purchase_pending
The purchase is running, but its confirmation is delayed.
Repeat the request later with the same Idempotency-Key. Do not create a new key for this purchase.
503
balance_busy
The balance reservation was cancelled under load; the purchase did not start.
Wait as Retry-After says and repeat with a new Idempotency-Key.
503
upstream_error
On a purchase: the service is busy and the purchase was declined. On a cancellation: an internal service is temporarily unavailable.
Wait as Retry-After says. For a new purchase attempt, create a new key. For a cancellation, check the status and repeat the request for the same order.
504
upstream_timeout
The service did not answer in time.
Repeat the purchase with the same Idempotency-Key.
Retry-After in 503 and 504 responses gives the recommended pause in seconds. Without the header, start with 3 seconds. After a network error or a purchase timeout, keep the same Idempotency-Key. After a failed cancellation, check the status first: the cancellation may already have gone through.
Limits
What
Limit
Request rate
About 10 requests per second from one IP, short bursts up to 60. Above the limit you get 429.
Active orders
GET /v1/numbers returns up to 200 orders.
History
size: up to 100 records per page.
Idempotency-Key
From 1 to 128 characters.
Rentals
GET /v1/rentals: size up to 100, 50 by default.
Test webhook
No more than once every 60 seconds.
Reference
All methods
Parameters, schemas and response examples for every method. Specification: openapi.en.yaml; you can import it into Postman or Insomnia.
Loading the interactive reference…
Catalogs
Services
Service codes for POST /v1/numbers and GET /v1/services/{code}/countries. Click a code to copy it.
The catalog loads when you open it
The catalog lists every code. Current prices and stock come from GET /v1/services.
Catalogs
Countries
Numeric country IDs for the country field. Click an ID to copy it.
The catalog loads when you open it
Found a mistake or have questions? Write to support.