Temp Number API - Phone Number, SMS & OTP
Provision virtual phone numbers, receive SMS in real time and automate OTP verification - from a clean REST API, or straight from your AI assistant.
https://api.temp-number.com/v1
Home / Developers
Introduction
The Temp Number API is organised around predictable resources, uses standard HTTP verbs and returns JSON for every response. One key gives read and write access to your account only - there is no cross-account access.
Quickstart
From zero to a verification code. Pick your language once - every sample on the page follows it.
-
Create an API key
In Dashboard → API Access, create a key. It is shown once: store it as the environment variable
TEMP_NUMBER_API_KEY. -
Check your balance
Every call carries your key in the Authorization header. Start with something harmless: your wallet balance.
curl --request GET "https://api.temp-number.com/v1/account" \ --header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \ --header "Accept: application/json"const response = await fetch("https://api.temp-number.com/v1/account", { headers: { Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`, Accept: "application/json", }, }); const result = await response.json(); console.log(response.status, result);import os import requests response = requests.get( "https://api.temp-number.com/v1/account", headers={ "Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}", "Accept": "application/json", }, timeout=30, ) print(response.status_code, response.json())use Illuminate\Support\Facades\Http; // config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')] $response = Http::withToken(config('services.temp_number.key')) ->acceptJson() ->get('https://api.temp-number.com/v1/account'); $result = $response->json(); -
Pick a country and check stock
GET /pricing lists every country with the exact price per duration. Then confirm stock for the one you want.
curl --request GET "https://api.temp-number.com/v1/inventory?country_code=US&duration=oneMonth" \ --header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \ --header "Accept: application/json"const response = await fetch("https://api.temp-number.com/v1/inventory?country_code=US&duration=oneMonth", { headers: { Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`, Accept: "application/json", }, }); const result = await response.json(); console.log(response.status, result);import os import requests response = requests.get( "https://api.temp-number.com/v1/inventory", headers={ "Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}", "Accept": "application/json", }, params={"country_code": "US", "duration": "oneMonth"}, timeout=30, ) print(response.status_code, response.json())use Illuminate\Support\Facades\Http; // config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')] $response = Http::withToken(config('services.temp_number.key')) ->acceptJson() ->get('https://api.temp-number.com/v1/inventory', [ 'country_code' => 'US', 'duration' => 'oneMonth', ]); $result = $response->json(); -
Buy a number
Send a fresh Idempotency-Key with every purchase. If the network drops, retry with the same key: you are never charged twice. Keep data.id from the response.
curl --request POST "https://api.temp-number.com/v1/numbers/buy" \ --header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: $(uuidgen)" \ --data '{"country_code":"US","duration":"oneMonth"}'import { randomUUID } from "node:crypto"; const response = await fetch("https://api.temp-number.com/v1/numbers/buy", { method: "POST", headers: { Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`, Accept: "application/json", "Content-Type": "application/json", "Idempotency-Key": randomUUID(), }, body: JSON.stringify({ country_code: "US", duration: "oneMonth" }), }); const result = await response.json(); console.log(response.status, result);import os import uuid import requests response = requests.post( "https://api.temp-number.com/v1/numbers/buy", headers={ "Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}", "Accept": "application/json", "Idempotency-Key": str(uuid.uuid4()), }, json={"country_code": "US", "duration": "oneMonth"}, timeout=60, ) print(response.status_code, response.json())use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; // config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')] $response = Http::withToken(config('services.temp_number.key')) ->acceptJson() ->withHeaders(['Idempotency-Key' => (string) Str::uuid()]) ->timeout(60) ->post('https://api.temp-number.com/v1/numbers/buy', [ 'country_code' => 'US', 'duration' => 'oneMonth', ]); $result = $response->json(); -
Read the SMS
Poll the number every few seconds until the message arrives. You get the raw text; pull the code out on your side.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/sms" \ --header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \ --header "Accept: application/json"const response = await fetch("https://api.temp-number.com/v1/numbers/1482/sms", { headers: { Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`, Accept: "application/json", }, }); const result = await response.json(); console.log(response.status, result);import os import requests response = requests.get( "https://api.temp-number.com/v1/numbers/1482/sms", headers={ "Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}", "Accept": "application/json", }, timeout=30, ) print(response.status_code, response.json())use Illuminate\Support\Facades\Http; // config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')] $response = Http::withToken(config('services.temp_number.key')) ->acceptJson() ->get('https://api.temp-number.com/v1/numbers/1482/sms'); $result = $response->json(); -
No SMS? Cancel for a refund
Between 15 and 120 minutes after purchase, a number that has received no SMS can be cancelled for a full refund to your wallet.
curl --request POST "https://api.temp-number.com/v1/numbers/1482/cancel" \ --header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \ --header "Accept: application/json" \ --header "Idempotency-Key: $(uuidgen)"import { randomUUID } from "node:crypto"; const response = await fetch("https://api.temp-number.com/v1/numbers/1482/cancel", { method: "POST", headers: { Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`, Accept: "application/json", "Idempotency-Key": randomUUID(), }, }); const result = await response.json(); console.log(response.status, result);import os import uuid import requests response = requests.post( "https://api.temp-number.com/v1/numbers/1482/cancel", headers={ "Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}", "Accept": "application/json", "Idempotency-Key": str(uuid.uuid4()), }, timeout=60, ) print(response.status_code, response.json())use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; // config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')] $response = Http::withToken(config('services.temp_number.key')) ->acceptJson() ->withHeaders(['Idempotency-Key' => (string) Str::uuid()]) ->timeout(60) ->post('https://api.temp-number.com/v1/numbers/1482/cancel'); $result = $response->json();
Authentication
Send your API key in the standard Authorization header on every request:
Authorization: Bearer YOUR_API_KEY
- Create keys under API Access. A key is shown in full once, at creation.
- Use one key per integration, so you can revoke one without breaking the others. Changing your password revokes every key.
- A missing, malformed or revoked key answers
401. An account that has not verified its email, or is suspended, answers403withcodeemail_unverifiedoraccount_suspended. - REST keys work on
/v1only. The MCP server uses its own sign-in or MCP keys - see MCP server.
Base URL & versioning
All endpoints live under the permanent /v1 prefix:
https://api.temp-number.com/v1
Additive changes - new fields, endpoints or optional parameters - ship without a version bump, so ignore fields you do not know. A breaking change would ship under /v2; your integration is never moved silently. The machine-readable contract is the OpenAPI spec.
Requests & responses
- Send JSON bodies with
Content-Type: application/json, andAccept: application/jsonon every request. - Success:
{ "success": true, "data": …, "meta": {…} }.metacarries counts, pagination and money summaries. - Errors:
{ "success": false, "message": "…", "code": "…" }. Branch oncode; the message is for people. See Errors. - Times are ISO 8601. Money is USD with two decimals - always the price you pay.
- Some objects still carry older field names for existing integrations (marked deprecated in the reference). New code should ignore them.
Errors
Errors use standard HTTP status codes. Business errors carry a stable, machine-readable code; some carry details with more context.
{
"success": false,
"message": "Insufficient wallet balance.",
"code": "insufficient_funds"
}
{
"message": "The country code field is required.",
"errors": {
"country_code": ["The country code field is required."]
}
}
| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | - | The Idempotency-Key header is missing or malformed on buy, renew or cancel. Body: {"error": "Missing Idempotency-Key header."} | Send a UUID in Idempotency-Key. |
| 401 | - | No bearer token, a malformed one, or a revoked key. | Check the Authorization header; create a new key if it was revoked. |
| 403 | email_unverified |
The account has not verified its email address yet. | Verify the email on the website, then retry. |
| 403 | account_suspended |
The account is suspended. | Contact support. |
| 403 | kyc_required |
The account must verify its identity before buying new numbers. Nothing was charged; renewals still work. | Complete identity verification in Account settings, then retry. |
| 403 | - | The key has no API access (an MCP key, for example), or GET /receive-sms was asked about a number on another account. | Use a REST API key; only query your own numbers. |
| 404 | - | The number, message or country is not on your account or not on sale right now. Ids of other accounts also answer 404. | Re-list with GET /numbers or GET /pricing. |
| 404 | private_email_closed |
The private inbox is closed because its number was cancelled or refunded. | Nothing to retry. |
| 409 | - | The Idempotency-Key was already used with a different body, or the first request with it is still running. | Use a new key for a new request; wait and retry for the in-flight one. |
| 422 | insufficient_funds |
The wallet cannot cover the price. | Top up the wallet and retry. |
| 422 | out_of_stock |
The market sold out between your price check and the purchase. | Re-check GET /inventory and pick another duration or type. |
| 422 | invalid_duration |
The duration is not sold for that country and number type. | Re-fetch GET /pricing/{countryCode}. |
| 422 | - | Validation failed (an `errors` object names each field) or a rule refused the action, such as cancelling outside the refund window. | Read `message`; fix the request rather than retrying it. |
| 429 | - | Rate limit reached. | Wait for the Retry-After header, then retry with backoff. |
| 503 | purchases_paused |
New purchases are paused platform-wide. Nothing was charged; renewals and cancels still work. | Retry after Retry-After (60 s). |
| 5xx | - | Something failed on our side. | Retry with the SAME Idempotency-Key - you will never be charged twice. |
Rate limits
Limits apply per key, and money-moving calls have a tighter limit per account on top:
| Applies to | Limit | Counted |
|---|---|---|
| Every endpoint | 60 / minute | per API key |
| Buy, renew, cancel | 10 / minute | per account, on top of the above |
| GET /receive-sms | 60 / minute | per API key |
| GET /receive-sms | 30 / minute | per API key and phone number |
| GET /receive-sms | 30 / minute | per IP address |
Over a limit you get 429 with a Retry-After header (seconds). Wait at least that long, then back off with jitter. Responses also carry X-RateLimit-Limit and X-RateLimit-Remaining.
Idempotency
POST /numbers/buy, /renew and /cancel require an Idempotency-Key header: a UUID you create for each new action. If the network drops, send the same request with the same key - you get the original response back (marked X-Idempotency-Replayed: true) and are never charged twice.
- Missing key:
400. Keys are up to 128 characters of letters, digits,- _ . : - Same key with a different body:
409. Same key while the first request is still running:409- wait a moment and retry. - Keys are remembered per account and endpoint. Never reuse one for a different purchase.
Pagination
GET /receive-sms uses cursor pagination, newest first. Read meta.hasMore; while it is true, send meta.nextCursor back as cursor with the same phoneNumber. Cursors are opaque - do not build or edit them.
GET https://api.temp-number.com/v1/receive-sms?phoneNumber=%2B14035551234&limit=50&cursor=eyJkYXRlIjoiMjAyNi0wNS0xMyJ9
Lists that are small by nature (/numbers, /pricing) return everything in one response; the private inbox uses page and per_page.
Acceptable use
The API is built for developers: verification and OTP flows, automated onboarding, QA and system testing, and any product that needs inbound SMS or phone numbers at scale.
- Use it for legitimate verification, development and testing only.
- No scams, spam, fraud or deception. Numbers used for scams or other abusive activity get the account banned immediately and without notice, associated numbers cancelled, and no refund is due.
- Not for regulated or identity-bound accounts. Our shared, temporary numbers do not support SMS verification for banking, payment, cryptocurrency, government or other services that verify your legal identity (KYC/AML) - use your own personal mobile number for those.
API reference
Every live /v1 endpoint. Requests are on the right in your language; responses are real shapes.
Account
Read your wallet balance and settlement currency.
/v1/account
Get account balance
Returns the available wallet balance and its currency. Check it before a purchase so you can stop early with a clear "top up" message.
Response fields-
balancenumberAvailable wallet balance.
-
currencystringSettlement currency, USD.
- 401Missing or invalid key.
curl --request GET "https://api.temp-number.com/v1/account" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/account", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/account",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/account');
$result = $response->json();
{
"success": true,
"balance": 124.50,
"currency": "USD"
}
Pricing & stock
Browse live markets, the exact price you pay per duration, and real-time stock.
The country pricing object
One country and every package you can buy there. price is the final amount your wallet is charged; upstream cost is never exposed.
Fields-
country_codestringISO 3166-1 alpha-2 code, e.g. US.
-
country_namestringDisplay name.
-
packages[].number_typeenumvoip or non_voip.
-
packages[].durationenumThe key you send to buy: halfDay, oneDay, oneWeek, oneMonth, threeMonths, sixMonths or oneYear. Each country offers its own set.
-
packages[].duration_labelstringHuman label, e.g. "1 Month".
-
packages[].pricenumberFinal price in USD.
-
packages[].original_pricenumber | nullThe price before a discount, when one applies.
-
packages[].updated_atnullReserved.
{
"country_code": "US",
"country_name": "United States",
"packages": [
{
"number_type": "voip",
"duration": "oneMonth",
"duration_label": "1 Month",
"price": 4.99,
"original_price": 5.99,
"updated_at": null
}
]
}
/v1/pricing
List pricing for all countries
The complete live catalog: a list of country pricing objects. The price field is the final user-facing rate in USD, exactly what your wallet is charged.
ReturnsA list of country pricing objects in data, with meta.count.
Errors- 401Missing or invalid key.
curl --request GET "https://api.temp-number.com/v1/pricing" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/pricing", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/pricing",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/pricing');
$result = $response->json();
{
"success": true,
"data": [
{
"country_code": "US",
"country_name": "United States",
"packages": [
{
"number_type": "voip",
"duration": "oneMonth",
"duration_label": "1 Month",
"price": 4.99,
"original_price": 5.99,
"updated_at": null
},
{
"number_type": "voip",
"duration": "threeMonths",
"duration_label": "3 Months",
"price": 12.99,
"original_price": null,
"updated_at": null
}
]
}
],
"meta": { "count": 1 }
}
/v1/pricing/{countryCode}
Get pricing for one country
Live pricing for a single country. A 404 means the country has no active route right now: treat it as a temporary "out of stock", not as "never sold".
Parameters-
countryCodestring path RequiredISO 3166-1 alpha-2 country code, e.g. US, GB, CA.
One country pricing object in data.
Errors- 404No active route for that country right now.
curl --request GET "https://api.temp-number.com/v1/pricing/GB" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/pricing/GB", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/pricing/GB",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/pricing/GB');
$result = $response->json();
{
"success": true,
"data": {
"country_code": "GB",
"country_name": "United Kingdom",
"packages": [
{
"number_type": "voip",
"duration": "oneMonth",
"duration_label": "1 Month",
"price": 5.49,
"original_price": null,
"updated_at": null
}
]
}
}
/v1/inventory
Check stock
Real-time stock for a country and duration, so you can disable a Buy button before anyone clicks it. Leave number_type out to get every type side by side in types[]. A green result is a strong signal, but the purchase itself is the final check.
Parameters-
country_codestring query RequiredISO 3166-1 alpha-2 country code.
-
durationenum query OptionalA duration key from GET /pricing/{countryCode}. Omit for the route default.
One ofhalfDayoneDayoneWeekoneMonththreeMonthssixMonthsoneYear -
number_typeenum query OptionalPin the check to one type. Omit to see all types.
One ofvoipnon_voip
-
stockinteger | nullNumbers available (summed over types[] when number_type is omitted).
-
availablebooleanTrue when at least one number can be bought.
-
statusstringHuman label: In Stock, Out of Stock, Coming Soon…
-
types[]arrayOnly when number_type is omitted: number_type, stock, available, status per type the market sells.
- 422Nothing on sale for that selection; details carries the stock fields.
curl --request GET "https://api.temp-number.com/v1/inventory?country_code=US&duration=oneMonth" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/inventory?country_code=US&duration=oneMonth", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/inventory",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
params={"country_code": "US", "duration": "oneMonth"},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/inventory', [
'country_code' => 'US',
'duration' => 'oneMonth',
]);
$result = $response->json();
{
"success": true,
"data": {
"country_code": "US",
"duration": "oneMonth",
"number_type": null,
"stock": 47,
"available": true,
"status": "In Stock",
"types": [
{ "number_type": "voip", "stock": 0, "available": false, "status": "Out of Stock" },
{ "number_type": "non_voip", "stock": 47, "available": true, "status": "In Stock" }
]
}
}
Numbers
List, buy, renew and cancel your private numbers. Money-moving calls are idempotent and replay-safe.
The number object
A number on your account. Buy, renew and cancel also return a money summary in meta: charged (or refunded), currency and wallet_balance_after.
Fields-
idintegerUse it in /numbers/{id}/… calls. Webhooks send it as number.rentalId.
-
phone_numberstringE.164, e.g. +14035551234.
-
country_codestring | nullISO 3166-1 alpha-2.
-
country_namestringDisplay name.
-
number_typeenumvoip or non_voip.
-
statusenumactive, expired, cancelled or refunded.
-
purchase_datestringISO 8601, UTC.
-
expire_datestringISO 8601, UTC. After it the number stops receiving SMS and cannot be renewed.
Deprecated aliases still sent for older integrations: country, provider, purchased_at and expires_at (epoch milliseconds). Do not build on them.
{
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "active",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-06-13T17:42:09Z"
}
/v1/numbers
List your numbers
Every number on your account, newest first. Use status=expiring_soon (active numbers within 10 days of expiry) to drive renewal reminders.
Parameters-
statusenum query OptionalLifecycle filter. Default all.
One ofallactiveexpiring_soonexpiredcancelled
A list of number objects in data; meta.count and meta.status.
Errors- 401Missing or invalid key.
curl --request GET "https://api.temp-number.com/v1/numbers?status=expiring_soon" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers?status=expiring_soon", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
params={"status": "expiring_soon"},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers', [
'status' => 'expiring_soon',
]);
$result = $response->json();
{
"success": true,
"data": [
{
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "active",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-06-13T17:42:09Z"
}
],
"meta": { "count": 1, "status": "expiring_soon" }
}
/v1/numbers/buy
Idempotency-Key required Buy a number
Allocates a new private number and charges your wallet in one atomic step. If fulfilment fails after the charge, the charge is reversed: you never end up paid-but-empty. The response shows the amount charged and your balance after.
Parameters-
country_codestring body RequiredA country from GET /pricing.
-
durationenum body RequiredA duration key from that country's packages[] - each country offers its own set.
One ofhalfDayoneDayoneWeekoneMonththreeMonthssixMonthsoneYear -
number_typeenum body OptionalOmit to use the route default.
One ofvoipnon_voip -
Idempotency-Keystring header RequiredA UUID, new for each purchase, reused on retries.
The new number object in data; meta.charged, meta.currency, meta.wallet_balance_after.
Errors- 422
insufficient_fundsThe wallet cannot cover the price. - 422
out_of_stockSold out since your stock check. - 422
invalid_durationDuration not sold for this country/type. - 403
kyc_requiredIdentity verification needed first. Nothing charged. - 503
purchases_pausedPurchases paused platform-wide. Retry-After: 60.
curl --request POST "https://api.temp-number.com/v1/numbers/buy" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen)" \
--data '{"country_code":"US","duration":"oneMonth","number_type":"voip"}'
import { randomUUID } from "node:crypto";
const response = await fetch("https://api.temp-number.com/v1/numbers/buy", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ country_code: "US", duration: "oneMonth", number_type: "voip" }),
});
const result = await response.json();
console.log(response.status, result);
import os
import uuid
import requests
response = requests.post(
"https://api.temp-number.com/v1/numbers/buy",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"country_code": "US", "duration": "oneMonth", "number_type": "voip"},
timeout=60,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->timeout(60)
->post('https://api.temp-number.com/v1/numbers/buy', [
'country_code' => 'US',
'duration' => 'oneMonth',
'number_type' => 'voip',
]);
$result = $response->json();
{
"success": true,
"data": {
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "active",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-06-13T17:42:09Z"
},
"meta": {
"charged": 4.99,
"currency": "USD",
"wallet_balance_after": 119.51
}
}
/v1/numbers/{id}/renew
Idempotency-Key required Renew a number
Extends a number by the chosen package. The new expiry is added to the current one, so renewing early loses nothing. The price is the current site rate, worked out on our side. Expired numbers cannot be renewed: buy a new one.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
packageenum body OptionalRenewal length. Default oneMonth. (duration is accepted as a legacy alias.)
One ofoneMonththreeMonthssixMonthsoneYear -
Idempotency-Keystring header RequiredA UUID, new for each renewal, reused on retries.
The updated number object in data; meta.charged, meta.currency, meta.wallet_balance_after.
Errors- 404Not a number on your account.
- 422
insufficient_fundsThe wallet cannot cover the renewal. - 422Expired, cancelled, or not renewable.
curl --request POST "https://api.temp-number.com/v1/numbers/1482/renew" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen)" \
--data '{"package":"threeMonths"}'
import { randomUUID } from "node:crypto";
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/renew", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ package: "threeMonths" }),
});
const result = await response.json();
console.log(response.status, result);
import os
import uuid
import requests
response = requests.post(
"https://api.temp-number.com/v1/numbers/1482/renew",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"package": "threeMonths"},
timeout=60,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->timeout(60)
->post('https://api.temp-number.com/v1/numbers/1482/renew', [
'package' => 'threeMonths',
]);
$result = $response->json();
{
"success": true,
"data": {
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "active",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-09-13T17:42:09Z"
},
"meta": {
"charged": 12.99,
"currency": "USD",
"wallet_balance_after": 106.52
}
}
/v1/numbers/{id}/cancel
Idempotency-Key required Cancel a number for a refund
Releases a number and refunds its full price to your wallet. Allowed only between 15 and 120 minutes after purchase, while the number is active and before it has received any SMS. Outside those rules the call is refused with 422 and nothing changes; too early, details.seconds_until_open says how long to wait.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
Idempotency-Keystring header RequiredA UUID, reused on retries.
The number object (status cancelled) in data; meta.refunded, meta.currency, meta.wallet_balance_after.
Errors- 404Not a number on your account.
- 422Too early (under 15 min), too late (over 120 min), already received SMS, or not active.
curl --request POST "https://api.temp-number.com/v1/numbers/1482/cancel" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json" \
--header "Idempotency-Key: $(uuidgen)"
import { randomUUID } from "node:crypto";
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
"Idempotency-Key": randomUUID(),
},
});
const result = await response.json();
console.log(response.status, result);
import os
import uuid
import requests
response = requests.post(
"https://api.temp-number.com/v1/numbers/1482/cancel",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=60,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->timeout(60)
->post('https://api.temp-number.com/v1/numbers/1482/cancel');
$result = $response->json();
{
"success": true,
"data": {
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "cancelled",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-06-13T17:42:09Z"
},
"meta": {
"refunded": 4.99,
"currency": "USD",
"wallet_balance_after": 124.50
}
}
SMS
Read the SMS your numbers receive. You get the raw text; extracting a code is up to you.
The SMS message object
One received SMS, as returned by GET /numbers/{id}/sms. (GET /receive-sms uses a camelCase variant, shown on that endpoint.)
Fields-
idintegerMessage id.
-
fromstringSender: a phone number or a name such as WhatsApp.
-
bodystringThe full message text.
-
received_atstringISO 8601, UTC.
Deprecated aliases still sent: sender (= from) and message (= body).
{
"id": 9912,
"from": "+18005550199",
"body": "Your code is 432901",
"received_at": "2026-05-13T17:43:14Z"
}
/v1/numbers/{id}/sms
Get a number and its SMS
One number together with every SMS it has received, newest first. Best for a "drill into this number" view, and for polling a single number while you wait for a code. Ids that are not yours answer 404.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
data.number (a number object) and data.messages (SMS message objects); meta.count.
Errors- 404Not a number on your account.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/sms" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/sms", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/sms",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers/1482/sms');
$result = $response->json();
{
"success": true,
"data": {
"number": {
"id": 1482,
"phone_number": "+14035551234",
"country_code": "US",
"country_name": "United States",
"number_type": "voip",
"status": "active",
"purchase_date": "2026-05-13T17:42:09Z",
"expire_date": "2026-06-13T17:42:09Z"
},
"messages": [
{
"id": 9912,
"from": "+18005550199",
"body": "Your code is 432901",
"received_at": "2026-05-13T17:43:14Z"
}
]
},
"meta": { "count": 1 }
}
/v1/receive-sms
Receive SMS by phone number
Cursor-paginated SMS for a number you own, addressed by its phone number instead of its id. Best for high-volume inboxes. A number that is not yours answers 403.
Parameters-
phoneNumberstring query RequiredE.164 or digits only, e.g. +14035551234. URL-encode the +.
-
limitinteger query OptionalPage size 1-100. Default 50.
-
cursorstring query Optionalmeta.nextCursor from the previous page.
-
data.messages[].idintegerMessage id.
-
data.messages[].fromstringSender.
-
data.messages[].tostringYour number.
-
data.messages[].messagestringThe full text.
-
data.messages[].receivedAtstringISO 8601.
-
meta.hasMore / nextCursorboolean / string | nullPass nextCursor as cursor for the next page.
- 403The number is not on your account.
- 422phoneNumber missing or invalid.
curl --request GET "https://api.temp-number.com/v1/receive-sms?phoneNumber=%2B14035551234&limit=50" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/receive-sms?phoneNumber=%2B14035551234&limit=50", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/receive-sms",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
params={"phoneNumber": "+14035551234", "limit": 50},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/receive-sms', [
'phoneNumber' => '+14035551234',
'limit' => 50,
]);
$result = $response->json();
{
"success": true,
"data": {
"phoneNumber": "+14035551234",
"purchaseStatus": "active",
"messages": [
{
"id": 1842,
"from": "WhatsApp",
"to": "+14035551234",
"message": "Your verification code is 483920.",
"receivedAt": "2026-05-13T17:43:14+00:00"
}
]
},
"meta": {
"count": 1,
"pageSize": 50,
"hasMore": false,
"nextCursor": null,
"prevCursor": null,
"requestedAt": "2026-05-13T17:43:20+00:00"
}
}
Private email
A free, receive-only [email protected] inbox for each private number. One address per number.
The private email object
The inbox attached to one number.
Fields-
addressstring | nullThe address, or null before it is claimed.
-
statusenumactive or none.
-
unread_countintegerUnread emails.
-
created_atstring | nullWhen the address was claimed.
-
eligiblebooleanTrue when the number can claim an address now.
-
linked_numberstringThe phone number it belongs to.
-
domainstringprivnum.com
-
suggestionsstring[]Name ideas while eligible.
{
"address": "[email protected]",
"status": "active",
"unread_count": 2,
"created_at": "2026-07-03T10:15:00+00:00",
"eligible": false,
"linked_number": "+14035551234",
"domain": "privnum.com",
"suggestions": []
}
/v1/numbers/{id}/private-email
Get the private email
The address of a number, or, before one is claimed, whether it can have one and some name suggestions.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
A private email object in data.
Errors- 404Not a number on your account.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/private-email" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/private-email",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers/1482/private-email');
$result = $response->json();
{
"success": true,
"data": {
"address": null,
"status": "none",
"unread_count": 0,
"created_at": null,
"eligible": true,
"linked_number": "+14035551234",
"domain": "privnum.com",
"suggestions": ["alex.smith", "alex.smith42"]
}
}
/v1/numbers/{id}/private-email/availability
Check a name
Live check that a name is valid and free before you claim it.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
local_partstring query RequiredThe part before @: 3-64 letters, numbers, dots, dashes or underscores.
-
availablebooleanTrue when you can claim it.
-
reasonenum | nulltoo_short, too_long, invalid, reserved or taken.
- 404Not a number on your account.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/private-email/availability?local_part=alex.smith" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email/availability?local_part=alex.smith", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/private-email/availability",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
params={"local_part": "alex.smith"},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers/1482/private-email/availability', [
'local_part' => 'alex.smith',
]);
$result = $response->json();
{
"success": true,
"data": {
"local_part": "alex.smith",
"address": "[email protected]",
"available": true,
"reason": null,
"message": "Available"
}
}
/v1/numbers/{id}/private-email
Claim an address
Gives an active number its address. Free, one per number, and a claimed address is never handed to anyone else.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
local_partstring body RequiredThe name you checked.
The private email object in data.
Errors- 404Not your number, or no address yet.
- 422Name taken or invalid (details.reason), the number already has an address, or it is not active.
curl --request POST "https://api.temp-number.com/v1/numbers/1482/private-email" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{"local_part":"alex.smith"}'
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ local_part: "alex.smith" }),
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.post(
"https://api.temp-number.com/v1/numbers/1482/private-email",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
json={"local_part": "alex.smith"},
timeout=60,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->timeout(60)
->post('https://api.temp-number.com/v1/numbers/1482/private-email', [
'local_part' => 'alex.smith',
]);
$result = $response->json();
{
"success": true,
"data": {
"address": "[email protected]",
"status": "active",
"unread_count": 0,
"created_at": "2026-07-03T10:15:00+00:00",
"eligible": false,
"linked_number": "+14035551234",
"domain": "privnum.com",
"suggestions": []
}
}
/v1/numbers/{id}/private-email/inbox
List emails
Email summaries, newest first, with page-based pagination and the unread count at the top level.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
pageinteger query OptionalPage number. Default 1.
-
per_pageinteger query Optional1-100. Default 25.
-
data[]arrayid, from_email, from_name, subject, preview, attachment_count, is_read, received_at.
-
metaobjectcurrent_page, per_page, total, last_page.
-
unread_countintegerTop level, next to data.
- 404Not your number, or no address yet.
- 404
private_email_closedThe number was cancelled or refunded.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/private-email/inbox?page=1&per_page=25" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email/inbox?page=1&per_page=25", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/private-email/inbox",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
params={"page": 1, "per_page": 25},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers/1482/private-email/inbox', [
'page' => 1,
'per_page' => 25,
]);
$result = $response->json();
{
"success": true,
"data": [
{
"id": 77,
"from_email": "[email protected]",
"from_name": "Example Billing",
"subject": "Your receipt",
"preview": "Thanks for your order…",
"attachment_count": 1,
"is_read": false,
"received_at": "2026-07-04T08:01:12+00:00"
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 },
"unread_count": 1
}
/v1/numbers/{id}/private-email/inbox/{messageId}/body
Read an email
The full email (HTML and text) with its attachment list. Reading it marks it read.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
messageIdinteger path RequiredEmail id from the inbox list.
- 404Not your number, or no address yet.
- 404
private_email_closedThe number was cancelled or refunded.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/body" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/body", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/body",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->get('https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/body');
$result = $response->json();
{
"success": true,
"data": {
"id": 77,
"subject": "Your receipt",
"from_email": "[email protected]",
"from_name": "Example Billing",
"received_at": "2026-07-04T08:01:12+00:00",
"html": "<p>Thanks for your order…</p>",
"text": "Thanks for your order…",
"attachments": [
{ "index": 0, "filename": "receipt.pdf", "content_type": "application/pdf", "size_bytes": 48211 }
]
}
}
/v1/numbers/{id}/private-email/inbox/{messageId}/attachments/{index}
Download an attachment
The raw file, with its own Content-Type and filename. index comes from the email's attachments[].
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
messageIdinteger path RequiredEmail id from the inbox list.
-
indexinteger path Requiredattachments[].index from Read an email.
- 404Not your number, or no address yet.
- 404
private_email_closedThe number was cancelled or refunded. - 404No attachment at that index.
curl --request GET "https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/attachments/0" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--output receipt.pdf
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/attachments/0", {
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
},
});
await writeFile("receipt.pdf", Buffer.from(await response.arrayBuffer()));
import os
import requests
response = requests.get(
"https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/attachments/0",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
},
timeout=30,
)
with open("receipt.pdf", "wb") as file:
file.write(response.content)
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->sink(storage_path('receipt.pdf'))
->get('https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77/attachments/0');
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename=receipt.pdf
<binary file>
/v1/numbers/{id}/private-email/inbox/{messageId}
Delete an email
Deletes the email and its attachments.
Parameters-
idinteger path RequiredNumber id from GET /numbers.
-
messageIdinteger path RequiredEmail id from the inbox list.
- 404Not your number, or no address yet.
- 404
private_email_closedThe number was cancelled or refunded.
curl --request DELETE "https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77" \
--header "Authorization: Bearer $TEMP_NUMBER_API_KEY" \
--header "Accept: application/json"
const response = await fetch("https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.TEMP_NUMBER_API_KEY}`,
Accept: "application/json",
},
});
const result = await response.json();
console.log(response.status, result);
import os
import requests
response = requests.delete(
"https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77",
headers={
"Authorization": f"Bearer {os.environ['TEMP_NUMBER_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
print(response.status_code, response.json())
use Illuminate\Support\Facades\Http;
// config/services.php: 'temp_number' => ['key' => env('TEMP_NUMBER_API_KEY')]
$response = Http::withToken(config('services.temp_number.key'))
->acceptJson()
->delete('https://api.temp-number.com/v1/numbers/1482/private-email/inbox/77');
$result = $response->json();
{
"success": true,
"data": null
}
AI & MCP
Let an AI assistant use your account directly, or hand a coding agent everything it needs to build on the API.
MCP server
Alongside the REST API we run a Model Context Protocol server. Connect it to Claude, ChatGPT or your editor and the assistant can check prices and stock, buy a number and read its SMS as native tools, with no code on your side.
Endpointhttps://mcp.temp-number.com/mcp
How sign-in works
- Add the URL to your AI app. It sends you to Temp Number to sign in with your own account (OAuth 2.1 with PKCE) - no key to copy.
- On the consent screen you choose what the assistant may do. Reading balance, prices, your numbers and their SMS is always included. Allow purchases - buying, renewing and cancelling with your wallet - is off unless you tick it.
- Every connection is listed under Dashboard → MCP Server → Connected apps, where you can disconnect it at any time.
- Prefer a key (CI, scripts, headless agents)? Create an MCP key on the same page and send it as
Authorization: Bearer <key>. MCP keys and REST API keys are not interchangeable.
Tools
| Tool | What it does | Needs "Allow purchases" |
|---|---|---|
get-balance | Retrieve the authenticated account wallet balance and billing currency. | No |
get-pricing | Return the active pricing catalog for all countries or a single ISO country code. | No |
get-inventory | Inspect live inventory availability for a country, duration, and optional number type. | No |
purchase-number | Purchase one private number for the authenticated account using wallet funds. | Yes |
list-active-numbers | Return numbers owned by the authenticated account, optionally filtered by status. | No |
check-sms | Fetch SMS messages for a purchased number ID owned by the authenticated account. | No |
renew-number | Renew a purchased number by ID using an optional duration package. | Yes |
cancel-number | Cancel a purchased number by ID and return any eligible wallet refund. | Yes |
Connect your AI tool
Pick your app. Most connect in one click: you sign in to Temp Number, tick what the assistant may do, and click Allow.
Claude - claude.ai, Desktop and mobile
Nothing to install. Works in the browser, the desktop app and on mobile.
- Click Add to Claude. Claude opens with Temp Number and its URL already filled in. Click Add.
- Click Connect, sign in to Temp Number, choose whether Claude may make purchases, and click Allow.
- In any chat, open the + menu → Connectors and switch Temp Number on. Try: "What does a US number cost for a month?"
No button? In Claude go to Customize → Connectors → Add custom connector and paste the MCP endpoint. Organisations with Claude's request-header option can send an MCP key as Authorization: Bearer <key> instead of signing in.
ChatGPT
Needs ChatGPT Business, Enterprise or Edu on the web. On Business, an admin or owner adds it.
- Copy the server URL below, then click Open ChatGPT to go to its Plugins page. ChatGPT has no link that fills this in for you.
- Click + → Add custom MCP server. Name it Temp Number and paste the URL under Connection.
- Set authentication to OAuth, accept the risk warning and click Create as a plugin.
- When ChatGPT asks, sign in to Temp Number, choose whether ChatGPT may make purchases, and click Allow.
- In a new chat, type @ and pick Temp Number. Try: "What does a US number cost for a month?"
https://mcp.temp-number.com/mcp
Claude Code
Add the server once, then sign in from inside Claude Code.
claude mcp add --transport http temp-number https://mcp.temp-number.com/mcp
- Start Claude Code and type
/mcp. - Choose temp-number → Authenticate. Your browser opens: sign in and click Allow.
Using an MCP key instead (CI / headless)
export TEMPNUMBER_MCP_KEY=your-mcp-key
claude mcp add --transport http temp-number \
https://mcp.temp-number.com/mcp \
--header "Authorization: Bearer $TEMPNUMBER_MCP_KEY"
Codex CLI
Add the server, then sign in. Codex stores the session for you.
codex mcp add temp-number --url https://mcp.temp-number.com/mcp
codex mcp login temp-number
Using an MCP key instead (CI / headless)
In ~/.codex/config.toml. The key is read from the environment, so the file is safe to share.
[mcp_servers.temp-number]
url = "https://mcp.temp-number.com/mcp"
bearer_token_env_var = "TEMPNUMBER_MCP_KEY"
Cursor
One click, then sign in when Cursor asks.
Or add it to .cursor/mcp.json (or ~/.cursor/mcp.json). Cursor then shows Needs login next to the server: click it, sign in and click Allow.
{
"mcpServers": {
"temp-number": {
"url": "https://mcp.temp-number.com/mcp"
}
}
}
Using an MCP key instead
{
"mcpServers": {
"temp-number": {
"url": "https://mcp.temp-number.com/mcp",
"headers": {
"Authorization": "Bearer ${env:TEMPNUMBER_MCP_KEY}"
}
}
}
}
VS Code (GitHub Copilot)
One click, then sign in the first time a tool runs.
Or add it to .vscode/mcp.json - note the key is servers, not mcpServers.
{
"servers": {
"temp-number": {
"type": "http",
"url": "https://mcp.temp-number.com/mcp"
}
}
}
Using an MCP key instead
VS Code asks for the key once and keeps it in its secret storage.
{
"servers": {
"temp-number": {
"type": "http",
"url": "https://mcp.temp-number.com/mcp",
"headers": {
"Authorization": "Bearer ${input:tempnumber-mcp-key}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "tempnumber-mcp-key",
"description": "Temp Number MCP key",
"password": true
}
]
}
Any other MCP app
Windsurf, Zed, LibreChat, your own agent…
- Add the MCP endpoint as a remote (streamable HTTP) server. Apps that support sign-in find it on their own at
https://mcp.temp-number.com/.well-known/oauth-protected-resourceand open the Temp Number consent screen. - Apps without sign-in support can send an MCP key as
Authorization: Bearer <key>.
https://mcp.temp-number.com/mcp
llms.txt & agent prompt
Building with an AI coding assistant? Give it the context below and it can integrate the API correctly in one go - auth, idempotency and rate limits included.
You are integrating the Temp Number REST API. Read the full, machine-readable reference before writing any code:
- API reference (llms.txt): https://temp-number.com/developers/llms.txt
- OpenAPI spec: https://api.temp-number.com/api.json
- Base URL: https://api.temp-number.com/v1
- MCP server (for AI agents that call tools directly): https://mcp.temp-number.com/mcp - sign in with OAuth, or send an MCP key as a Bearer token.
Rules: authenticate every request with `Authorization: Bearer <YOUR_API_KEY>` (created in the dashboard → API Access) and keep the key in an environment variable, never in code; send and accept JSON over HTTPS; send a unique `Idempotency-Key` (UUID) header on every POST that buys, renews or cancels - it is required, and retries must reuse the same key; branch on the `code` field of error responses, not on the message text; on HTTP 429 or 503, wait for the `Retry-After` header. All prices are final USD.
Task: <describe what you want to build - e.g. "buy a US number, wait for the verification SMS, then cancel it if it never arrives">.
https://temp-number.com/developers/llms.txt is an always-current, plain-text summary of every endpoint; the OpenAPI spec is the typed contract. Both update with the API.Resources
What changed, and where to get help.
Changelog
Additive changes only - nothing on /v1 has been removed.
-
NewMCP: sign in with your Temp Number account
One-click "Add to Claude", plus ChatGPT, Claude Code, Codex, Cursor and VS Code without copying a key. You choose on the consent screen whether the AI may spend.
-
DocsA new home for the docs
Quickstart, search, and every sample in cURL, Node.js, Python and PHP. Errors, rate limits and refunds now match the live API exactly.
-
Newllms.txt for the whole site
A plain-text map of the service for AI assistants, linking to the agent-ready API reference.
-
NewConnect your AI tool
Setup for Claude Code, Codex, Cursor and VS Code, with one-click install links for Cursor and VS Code.
-
NewOpenAPI spec
The machine-readable contract is served at https://api.temp-number.com/api.json.
-
ImprovedObjects match the reference
Numbers and SMS use ISO 8601 dates and from/body. Errors carry a machine-readable code (out_of_stock, insufficient_funds, invalid_duration). /inventory reports every number type when you leave number_type out. The old field names still work as deprecated aliases.
-
NewPrivate email endpoints
Claim a free [email protected] inbox for a number, read mail and download attachments over the API.
-
NewMCP server
AI agents can check prices, buy numbers and read SMS as tools, using an MCP key.
-
LaunchPublic REST API v1
Account, pricing, inventory, the number lifecycle and SMS, with Bearer-token auth.
Support
Stuck, or found something in these docs that does not match what the API does? Contact us - include the endpoint, the time of the request and the response body (never your API key).
Ready to build?
Create a key and make your first call in under a minute.