API v2
2026-08-09Base URLhttps://juicysms.com/api/v2
Every path on this page is relative to it. Amounts are in EUR, timestamps are UTC, and this documentation describes version 2026-08-09.
Building this with an AI assistant? Give it the spec.
The fastest way to integrate is to hand your model https://juicysms.com/api/v2/openapi.json and ask it to write the client. That document is the whole contract β every endpoint, every parameter with its type and whether it is required, every error code, and worked examples. It is generated into this page too, so there is nothing here it does not know.
Tell it to use only that file. We ran an older API for years, and a model may recognise it and confidently write ?key= calls against endpoints like /api/makeorder. Those still work, but they are not this API, and mixing the two is the single most common way an AI-written integration fails.
Overview
Two products, one API. One-time numbers receive a single verification code and last 10 minutes; you are charged only if a message arrives. Rentals hold a real mobile number for a month or longer and are charged when you buy them.
Every response is JSON. Successes are an object carrying an object field naming its type; failures are application/problem+json with a stable code to branch on. Amounts are always EUR, timestamps always UTC.
There is an older plain-text API β ?key= in the query string, magic strings in the body, HTTP 200 on failure. It is still live and still supported, and nothing about it is changing; it is just not where a new integration should start. If you are already on it, its reference is at the legacy API page. Your existing key works here unchanged, as a bearer token.
Authentication
Every authenticated request carries your key in the Authorization header. Keys are never accepted in the query string: a URL ends up in web server logs, proxy logs, Referer headers and the shell history of whoever you paste an example to.
curl https://juicysms.com/api/v2/account \
-H "Authorization: Bearer $JUICYSMS_KEY"Two kinds of key work
Your account key
The 32-character key on your account page. One per account, unscoped β it can call every endpoint on this page. Rotating it invalidates the old one immediately, everywhere.
Scoped v2 keys
Create them under Account βΊ API keys. Each key can be narrowed to the scopes one integration needs, given an expiry date, and revoked on its own without disturbing anything else. The key is shown once, when you create it.
The cheapest way to check a key is GET /api/v2/account: it is the only endpoint that reports back which scopes the key you presented actually holds, so you can verify permissions without provoking a 403 from the endpoint you really wanted.
| If | Code | HTTP |
|---|---|---|
| No Authorization header | unauthenticated | 401 |
| The key matches nothing | invalid_token | 401 |
| The key has passed its expiry date | token_expired | 401 |
| The account is restricted | account_restricted | 403 |
| The key is not scoped for this endpoint | insufficient_scope | 403 |
Errors
Every failure is a application/problem+json document (RFC 9457) with the right HTTP status. There are no 200-with-a-string errors anywhere in v2, so your HTTP client, your retry middleware and your monitoring all see a failure as a failure.
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://juicysms.com/api/errors#insufficient_balance",
"title": "Insufficient balance",
"status": 402,
"code": "insufficient_balance",
"detail": "Your balance does not cover this order.",
"retryable": false,
"price": { "amount": "0.50", "amount_minor": 50, "currency": "EUR" },
"balance": { "amount": "0.12", "amount_minor": 12, "currency": "EUR" }
}codeis the field to branch on. These strings are part of the contract: we add to them, we never rename or repurpose one.retryabletells you whether repeating the identical request could ever succeed, so you do not have to keep your own table of which of our failures are transient.- Anything after
retryableis extra context for that specific failure βpriceandbalancehere,order_idon a concurrency conflict,errorson a validation failure. - The HTTP status for a given
codeis fixed. One condition cannot be a 409 on one endpoint and a 422 on another. statusalways means the HTTP status and nothing else. Where an order's own state is relevant β refusing to cancel an order that already finished, for instance β it is reported separately asorder_status, so the two can never be confused.
The codes
This is the complete list. Nothing else can come back.
| Code | HTTP | Retryable | Means |
|---|---|---|---|
| unauthenticated | 401 | no | No API key was presented. |
| invalid_token | 401 | no | The key presented is not a valid key. |
| token_expired | 401 | no | The key has passed the expiry you set on it. |
| insufficient_balance | 402 | no | Your balance does not cover this. The problem document includes price and balance. |
| price_above_maximum | 422 | no | The current price is above the max_price you sent. Nothing was bought. It is a 422 rather than a 402 because your balance is fine β the constraint you sent is what rejected it. |
| insufficient_scope | 403 | no | This key is not scoped for this endpoint. |
| account_restricted | 403 | no | The account cannot transact. Contact support. |
| feature_unavailable | 403 | no | The feature is not switched on for this account. Currently returned by every /webhook-endpoints route. |
| not_found | 404 | no | No such endpoint or resource. |
| order_not_found | 404 | no | No order with that id on your account. |
| rental_not_found | 404 | no | No rental with that id on your account. |
| service_not_found | 404 | no | No service with that id. |
| webhook_endpoint_not_found | 404 | no | No webhook endpoint with that id on your account. |
| method_not_allowed | 405 | no | Wrong HTTP verb for this path. The Allow header lists the right ones. |
| out_of_stock | 409 | yes | No number is available for that service and country right now. Try again shortly or try another country. |
| concurrent_order_limit | 409 | no | You already have an open order. Its id is in the problem document. Ask support about parallel ordering. |
| order_not_open | 409 | no | The order is already completed, cancelled or expired, so it cannot be changed. |
| number_unavailable | 409 | no | The number behind this order is gone and cannot be handed back to you. |
| rental_expired | 409 | no | The rental has already lapsed. Contact support to recover it. |
| unsupported_media_type | 415 | no | Send JSON. |
| validation_failed | 422 | no | The payload is wrong. The errors member names the offending fields. |
| country_not_supported | 422 | no | We do not sell that country for that product. |
| order_not_completed | 422 | no | Only an order that received a message can be reused. |
| reuse_not_supported | 422 | no | The number behind that order can no longer be identified, so it cannot be reused. |
| invalid_package | 422 | no | Unknown rental package. The valid keys are listed in the problem document. |
| rate_limited | 429 | yes | Too many requests. Wait for Retry-After. |
| internal_error | 500 | yes | Our fault. Retry shortly; contact support if it persists. |
| service_busy | 503 | yes | Another order for the same service and country is being placed. Retry in about a second. |
| maintenance | 503 | yes | Temporarily down for maintenance. |
Quickstart: order a number, read the code
- Look up the service id and what it costs you:
GET /services?country=UK. - Buy a number:
POST /orders. - Poll
GET /orders/{id}/messagesuntildatais non-empty, or register a webhook and skip the polling.
export JUICYSMS_KEY="your_api_key"
# 1. What can I buy, and what does it cost me?
curl -s "https://juicysms.com/api/v2/services?country=UK&search=whatsapp" \
-H "Authorization: Bearer $JUICYSMS_KEY"
# 2. Buy a number.
curl -s -X POST https://juicysms.com/api/v2/orders \
-H "Authorization: Bearer $JUICYSMS_KEY" \
-H "Content-Type: application/json" \
-d '{"service_id": 1, "country": "UK", "max_price": "0.60"}'
# 3. Poll until data is non-empty. Empty + "pending" means keep waiting.
curl -s https://juicysms.com/api/v2/orders/8123456/messages \
-H "Authorization: Bearer $JUICYSMS_KEY"
# Nothing arriving? Release the number and get a different one.
curl -s -X POST https://juicysms.com/api/v2/orders/8123456/skip \
-H "Authorization: Bearer $JUICYSMS_KEY"What comes back
POST /orders answers 201 with a Location header and the order itself:
{
"object": "order",
"id": 8123456,
"status": "pending",
"service": { "id": 1, "name": "WhatsApp" },
"country": "UK",
"country_iso": "GB",
"phone_number": "+447407792510",
"phone_number_local": "7407792510",
"price": { "amount": "0.50", "amount_minor": 50, "currency": "EUR" },
"charged": false,
"reused_from_order_id": null,
"created_at": "2026-08-09T12:00:00Z",
"expires_at": "2026-08-09T12:10:00Z",
"code": null,
"messages": []
}And once the message lands:
{
"data": [
{
"object": "message",
"id": 44120931,
"sender": "WhatsApp",
"text": "Your WhatsApp code is 417-097. Don't share it.",
"code": "417097",
"received_at": "2026-08-09T12:01:44Z",
"source": "sms"
}
],
"order_id": 8123456,
"order_status": "completed"
}codeis the extracted verification code, so you do not have to write a regex against the message body. It is deliberately conservative β one unambiguous run of 4β8 digits, a hyphen in the middle tolerated. When a message has no clear single candidate it isnullandtextis always there to fall back on.- An empty
dataarray withorder_status: "pending"means keep waiting. Any other status means the order is over and no message is coming. phone_numberis E.164 andphone_number_localis the national form, because some signup forms reject one or the other.- Orders stay open for 10 minutes. After that the order expires on its own, and you are not charged.
One order at a time, unless you ask
By default an account may hold one open order at a time. Ordering again while one is still running returns 409 concurrent_order_limit, with the id of the order already open in the problem document β so you can pick that one back up rather than starting over.
That limit can be lifted. Parallel ordering is a per-account setting: contact support and we can enable it, after which you can hold as many open orders at once as you need. It suits anyone verifying in bulk rather than one signup at a time. Read parallel_orders_allowed from GET /account to see which mode your account is in, rather than discovering it from a 409 in production.
Rate limits
Limits are per key, per minute, in separate buckets. Reads are generous because polling for a code is cheap; ordering is tighter because every call can allocate a real number.
| Bucket | Per minute | Applies to |
|---|---|---|
| api-v2-read | 240 | Every authenticated GET, including polling for a code |
| api-v2-write | 60 | Cancel, skip, rental updates, webhook endpoint changes |
| api-v2-order | 30 | Creating and reusing orders, creating and extending rentals |
| api-v2-public | 30 | The public discovery endpoints, counted per IP |
Every response tells you where you stand, in both the widely-used X-RateLimit-* form and the IETF RateLimit-* form. Note the difference in the reset field: X-RateLimit-Reset is an absolute Unix timestamp, RateLimit-Reset is seconds from now.
X-RateLimit-Limit: 240
X-RateLimit-Remaining: 237
X-RateLimit-Reset: 1786636800
RateLimit-Limit: 240
RateLimit-Remaining: 237
RateLimit-Reset: 41
RateLimit-Policy: 240;w=60 Going over is rate_limited (429) with a Retry-After header. Wait that long; do not retry immediately.
Other headers on every response
| Header | Meaning |
|---|---|
| X-Api-Version | The contract version this response was produced under. |
| Location | On every 201, the URL of the thing that was just created. |
| Retry-After | On a 429, and on the retryable 503s. Seconds to wait before trying again. |
| Cache-Control | no-store, private on everything that carries account data. Do not cache it. |
| Vary | Origin, Accept β so a shared cache in front of you cannot mix up responses. |
Cursor pagination
GET /orders and GET /rentals are cursor-paginated: pass limit (default 25, maximum 100) and cursor. There is no page number and no total count, on purpose β a cursor stays correct while new rows arrive underneath you, and costs the same on page 400 as on page 1.
GET /api/v2/orders?limit=25&status=completed
{
"data": [ "...25 orders, newest first..." ],
"pagination": {
"limit": 25,
"has_more": true,
"next_cursor": "eyJpZCI6ODEyMzQ1NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"previous_cursor": null
}
} Keep passing next_cursor back as cursor until has_more is false. Treat the cursor as opaque β do not parse it or build one yourself.
Small, bounded lists β services, rental packages, webhook endpoints and the messages on one order β come back without a pagination block. They are still JSON objects with a data array rather than bare arrays, so we can add fields to them later without breaking your client.
Webhooks
Not switched on yet. Every endpoint in this section currently answers 403 feature_unavailable. Poll GET /orders/{id}/messages for now β the quickstart above shows the loop β and talk to support if you want to be in the first group when webhooks open up. The section is documented in full so you can build against it in advance; nothing here will change when it is enabled.
Register an HTTPS URL and we push events to it, so you can stop polling. Create one with POST /webhook-endpoints, naming the events you want. You can have up to 5 endpoints.
curl -s -X POST https://juicysms.com/api/v2/webhook-endpoints \
-H "Authorization: Bearer $JUICYSMS_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/juicysms",
"events": ["sms.received", "rental.sms.received"],
"description": "production worker"
}'The signing secret is shown exactly once, in the response to that create call. It never appears again in any read endpoint. Store it when you create the endpoint; if you lose it, delete the endpoint and make a new one.
URLs must be https, on port 80 or 443, with no credentials in the URL, and must not resolve to a private, loopback, link-local or otherwise reserved address. We do not follow redirects.
Events
| Event | Fires when |
|---|---|
| sms.received | A message arrived on a one-time order β which is also the moment the order completed and your balance was charged. This is the event that replaces polling. |
| rental.sms.received | A message arrived on a long-term rental. |
| rental.expiring | A rental is within three days of expiry and will not renew automatically. Sent once per expiry window, alongside the email. |
POST /webhook-endpoints/{id}/test queues a ping delivery and answers 202 with its delivery id, so you can prove your receiver works before any real traffic depends on it. ping is not subscribable β it is only ever produced by that call. Testing a disabled endpoint is a 422, because nothing would go out.
What we send
POST /hooks/juicysms
JuicySMS-Signature: t=1786636800,v1=6f3a...c19b
JuicySMS-Event: sms.received
JuicySMS-Delivery-Id: 90210
User-Agent: JuicySMS-Webhook/2.0
Content-Type: application/json
{
"id": "whd_90210",
"object": "event",
"type": "sms.received",
"api_version": "2026-08-09",
"created_at": "2026-08-09T12:01:44Z",
"attempt": 1,
"data": {
"order": {
"object": "order",
"id": 8123456,
"status": "completed",
"service": { "id": 1, "name": "WhatsApp" },
"country": "UK",
"country_iso": "GB",
"phone_number": "+447407792510",
"phone_number_local": "7407792510"
},
"message": {
"object": "message",
"id": null,
"sender": "WhatsApp",
"text": "Your WhatsApp code is 417-097. Don't share it.",
"code": "417097",
"received_at": "2026-08-09T12:01:44Z",
"source": "provider"
}
}
} Answer 2xx quickly. Anything else counts as a failure and is retried up to 6 times with a backoff of 10s, 1m, 5m, 30m, then 2h. Our request times out after 10 seconds, so acknowledge first and do your work afterwards. An endpoint that keeps failing is disabled automatically; re-enable it with a PATCH. Recent attempts, with the status and body we got back, are listed at GET /webhook-endpoints/{id}/deliveries.
JuicySMS-Delivery-Id stays the same across every retry of the same event, so it is the key to de-duplicate on if your handler is not already idempotent. What does change per attempt is the attempt counter in the body β which means each attempt is a distinct message with its own signature, and you cannot cache one signature and compare it to the next.
Verifying the signature
v1 is the hex HMAC-SHA256 of timestamp + "." + raw request body, keyed with your endpoint secret. The timestamp is inside the signed string, which is what stops someone replaying a captured delivery forever: reject anything whose t is more than 300 seconds away from your clock, and compare the digests in constant time.
Sign the raw bytes of the request. If you parse the JSON and re-serialize it, whitespace and key order change and the signature will not match.
import hmac, hashlib, time
def verify(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
"""raw_body must be the exact bytes we sent, not a re-serialized dict."""
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp, received = parts.get("t"), parts.get("v1")
if not timestamp or not received:
return False
# Reject anything too old to be a live delivery: this is what makes
# replaying a captured request useless.
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(
secret.encode(),
timestamp.encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received)Scopes
A v2 key created under Account βΊ API keys holds only the scopes you tick. A key that only reads orders cannot spend your balance, which is what you want for anything that runs on a machine you do not fully control.
| Scope | Grants |
|---|---|
| account:read | Read your account and balance |
| services:read | Read the service catalogue and pricing |
| orders:read | Read orders and their messages |
| orders:write | Create, cancel, skip and reuse orders |
| rentals:read | Read rentals and their messages |
| rentals:write | Create, extend and modify rentals |
| webhooks:read | Read webhook endpoints |
| webhooks:write | Create, modify and delete webhook endpoints |
Calling an endpoint your key is not scoped for is insufficient_scope (403), and the problem document names both what was required and what your key holds. The account key is unscoped and passes every check.
Endpoint reference
Base URL https://juicysms.com/api/v2. Anything not listed here answers not_found as a problem document, never an HTML page.
Everything below β the endpoints, their scopes and their parameters β is generated from the OpenAPI document, so it cannot drift from the API. Import that URL into Postman, Insomnia or a client generator and you get the same contract, typed.
Discovery
Unauthenticated endpoints for finding out what this API is and whether it is up.
GET/no authentication
Describe the API
Takes no parameters.
GET/healthno authentication
Liveness check
Takes no parameters.
GET/openapi.jsonno authentication
Fetch this OpenAPI document
Takes no parameters.
Account
The caller's own account and balance.
GET/accountaccount:read
Retrieve the authenticated account
Takes no parameters.
Catalog
What is on sale, where, and at what price for this account.
GET/servicesservices:read
List services and their prices
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| country | query | string | Optional | Price the catalogue for this country. Accepts the storage code (`USA`, `UK`, `NL`, `PH`), the ISO code (`GB`, `US`), or the English name, in any casing. Omit to list the catalogue unpriced. |
| search | query | string | Optional | Case-insensitive substring match against the service name. Matches anywhere in the name, not just the start. |
Orders
One-time verification numbers. You are charged only if a message arrives.
GET/ordersorders:read
List your orders
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| created_after | query | string (date-time) | Optional | Only orders created at or after this instant. Any parseable date-time; send ISO 8601 with an offset to avoid ambiguity. |
| created_before | query | string (date-time) | Optional | Only orders created at or before this instant. |
| cursor | query | string | Optional | The `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing. |
| limit | query | integer | Optional | Rows per page. Defaults to 25, capped at 100. |
| service_id | query | integer | Optional | Only orders for this service. See `GET /services`. |
| status | query | pending | completed | canceled | expired | Optional | Only orders in this state. |
POST/ordersorders:write
Order a one-time number
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| country | body | string | Required | Storage code, ISO code, alpha-3 or English name, any casing. Must be a country sold one-time: `USA`, `UK`, `NL`, `PH`, `PL`, or the unlisted `DE`. Required. An unrecognised country is rejected rather than substituted. |
| service_id | body | integer | Required | From `GET /services`. |
| max_price | body | string or number | Optional | A ceiling on what you are willing to pay, in EUR major units. If the current price exceeds it, nothing is allocated and the call fails with `price_above_maximum`. Without it, an integration that has been running for a year silently accepts whatever the price has become. |
GET/orders/{order}orders:read
Retrieve an order
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| order | path | integer | Required | The order id, as returned by `POST /orders`. |
GET/orders/{order}/messagesorders:read
Read the messages on an order
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| order | path | integer | Required | The order id, as returned by `POST /orders`. |
POST/orders/{order}/cancelorders:write
Cancel an order
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| order | path | integer | Required | The order id, as returned by `POST /orders`. |
POST/orders/{order}/skiporders:write
Cancel an order and blacklist the number
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| order | path | integer | Required | The order id, as returned by `POST /orders`. |
POST/orders/{order}/reuseorders:write
Order the same number again, at half price
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| order | path | integer | Required | The order id, as returned by `POST /orders`. |
Rentals
Long-term number rentals, charged up front per package.
GET/rental-packagesrentals:read
List rental packages
Takes no parameters.
GET/rentalsrentals:read
List your rentals
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| cursor | query | string | Optional | The `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing. |
| limit | query | integer | Optional | Rows per page. Defaults to 25, capped at 100. |
| status | query | active | expired | all | Optional | Which rentals to include. `active` means the expiry is in the future. Defaults to `all`. |
POST/rentalsrentals:write
Rent a number
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| auto_renew | body | boolean | Required | Required, not optional. Required rather than defaulted, because a recurring charge rides on it. |
| country | body | string | Required | A country that can be rented. In practice only NL and UK carry for-hire stock. |
| package | body | string | Required | A `key` from `GET /rental-packages`. |
GET/rentals/{rental}rentals:read
Retrieve a rental
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| rental | path | integer | Required | The rental contract id, as returned by `POST /rentals`. |
PATCH/rentals/{rental}rentals:write
Change renewal settings
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| rental | path | integer | Required | The rental contract id, as returned by `POST /rentals`. |
| auto_renew | body | boolean | Optional | Whether to charge for another term when this one ends. Turning it off is how you cancel: the number stays yours until `expires_at`. |
| renewal_package | body | string | Optional | Which package the automatic renewal should buy. May differ from the package currently in force. |
GET/rentals/{rental}/messagesrentals:read
Read the messages on a rental
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| rental | path | integer | Required | The rental contract id, as returned by `POST /rentals`. |
POST/rentals/{rental}/extendrentals:write
Add time to a rental
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| package | body | string | Required | A `key` from `GET /rental-packages`. Its days are added to the current expiry, not to today. This does not change `renewal_package`. |
| rental | path | integer | Required | The rental contract id, as returned by `POST /rentals`. |
Webhooks
Push delivery of events, so you do not have to poll for messages.
GET/webhook-endpointswebhooks:read
List your webhook endpoints
Takes no parameters.
POST/webhook-endpointswebhooks:write
Register a webhook endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| events | body | array of sms.received | rental.sms.received | rental.expiring | Required | Which events to receive. At least one, and every name must be from the catalogue β there is no wildcard on registration, so a new event type needs a PATCH before it reaches you. |
| url | body | string (uri) | Required | HTTPS, port 80 or 443, no credentials in the URL, and the host must not resolve to a private, loopback, link-local or reserved address. Deliveries time out after 10 seconds, so acknowledge first and do your work afterwards. |
| description | body | string | Optional | Free-text label for your own bookkeeping. Never sent to the endpoint. |
| enabled | body | boolean | Optional | Register the endpoint switched off β useful for provisioning before the receiver is deployed. |
GET/webhook-endpoints/{endpoint}webhooks:read
Retrieve a webhook endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| endpoint | path | integer | Required | The webhook endpoint id. |
PATCH/webhook-endpoints/{endpoint}webhooks:write
Update a webhook endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| endpoint | path | integer | Required | The webhook endpoint id. |
| description | body | string | Optional | |
| enabled | body | boolean | Optional | False pauses delivery without discarding the configuration or rotating the secret. True also re-arms an endpoint our circuit breaker switched off, clearing `disabled_at` and resetting `consecutive_failures`. |
| events | body | array of sms.received | rental.sms.received | rental.expiring | Optional | |
| url | body | string (uri) | Optional | Validated exactly as strictly as on create. |
DELETE/webhook-endpoints/{endpoint}webhooks:write
Delete a webhook endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| endpoint | path | integer | Required | The webhook endpoint id. |
GET/webhook-endpoints/{endpoint}/deliverieswebhooks:read
List recent deliveries to an endpoint
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| endpoint | path | integer | Required | The webhook endpoint id. |
| cursor | query | string | Optional | The `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing. |
| limit | query | integer | Optional | Rows per page, clamped to 1-100 rather than rejected. Defaults to 25. |
POST/webhook-endpoints/{endpoint}/testwebhooks:write
Send a test delivery
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| endpoint | path | integer | Required | The webhook endpoint id. |
Countries
The list changes very rarely, so it lives here rather than behind an endpoint.
| Code | Country | Dialling | Available for |
|---|---|---|---|
| NL | Netherlands | +31 | One-time, Rentals |
| UK | United Kingdom | +44 | One-time, Rentals |
| USA | United States | +1 | One-time |
| PH | Philippines | +63 | One-time |
| PL | Poland | +48 | One-time |
There is no stock figure anywhere in the API, for one-time numbers or for rentals. Stock moves second to second and any number published here would be wrong by the time you read it. Place the order and handle out_of_stock, which is marked retryable because it usually clears on its own.
Country codes are ours, not ISO β UK rather than GB, USA rather than US β but requests also accept the ISO form and the full English name, in any casing, and every response carries both country and country_iso. A country we do not sell is a 422; we never quietly substitute a different one.
Money
Every amount β prices, balances, rental packages β is the same object, and always in EUR, which is the currency your balance is actually held and charged in. Nothing is converted.
{
"amount": "0.50",
"amount_minor": 50,
"currency": "EUR"
} Use amount_minor (an integer number of cents) for arithmetic and amount for display. There is no float anywhere in the payload. Timestamps are ISO 8601 in UTC (2026-08-09T12:10:00Z), and booleans are real JSON booleans.
