4.7 / 5 β€’ 625 reviews

API v2

2026-08-09
A modern JSON API for ordering real mobile numbers and reading verification codes.

Base 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.

IfCodeHTTP
No Authorization headerunauthenticated401
The key matches nothinginvalid_token401
The key has passed its expiry datetoken_expired401
The account is restrictedaccount_restricted403
The key is not scoped for this endpointinsufficient_scope403

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" }
}
  • code is the field to branch on. These strings are part of the contract: we add to them, we never rename or repurpose one.
  • retryable tells 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 retryable is extra context for that specific failure β€” price and balance here, order_id on a concurrency conflict, errors on a validation failure.
  • The HTTP status for a given code is fixed. One condition cannot be a 409 on one endpoint and a 422 on another.
  • status always 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 as order_status, so the two can never be confused.

The codes

This is the complete list. Nothing else can come back.

CodeHTTPRetryableMeans
unauthenticated401noNo API key was presented.
invalid_token401noThe key presented is not a valid key.
token_expired401noThe key has passed the expiry you set on it.
insufficient_balance402noYour balance does not cover this. The problem document includes price and balance.
price_above_maximum422noThe 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_scope403noThis key is not scoped for this endpoint.
account_restricted403noThe account cannot transact. Contact support.
feature_unavailable403noThe feature is not switched on for this account. Currently returned by every /webhook-endpoints route.
not_found404noNo such endpoint or resource.
order_not_found404noNo order with that id on your account.
rental_not_found404noNo rental with that id on your account.
service_not_found404noNo service with that id.
webhook_endpoint_not_found404noNo webhook endpoint with that id on your account.
method_not_allowed405noWrong HTTP verb for this path. The Allow header lists the right ones.
out_of_stock409yesNo number is available for that service and country right now. Try again shortly or try another country.
concurrent_order_limit409noYou already have an open order. Its id is in the problem document. Ask support about parallel ordering.
order_not_open409noThe order is already completed, cancelled or expired, so it cannot be changed.
number_unavailable409noThe number behind this order is gone and cannot be handed back to you.
rental_expired409noThe rental has already lapsed. Contact support to recover it.
unsupported_media_type415noSend JSON.
validation_failed422noThe payload is wrong. The errors member names the offending fields.
country_not_supported422noWe do not sell that country for that product.
order_not_completed422noOnly an order that received a message can be reused.
reuse_not_supported422noThe number behind that order can no longer be identified, so it cannot be reused.
invalid_package422noUnknown rental package. The valid keys are listed in the problem document.
rate_limited429yesToo many requests. Wait for Retry-After.
internal_error500yesOur fault. Retry shortly; contact support if it persists.
service_busy503yesAnother order for the same service and country is being placed. Retry in about a second.
maintenance503yesTemporarily down for maintenance.

Quickstart: order a number, read the code

  1. Look up the service id and what it costs you: GET /services?country=UK.
  2. Buy a number: POST /orders.
  3. Poll GET /orders/{id}/messages until data is 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"
}
  • code is 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 is null and text is always there to fall back on.
  • An empty data array with order_status: "pending" means keep waiting. Any other status means the order is over and no message is coming.
  • phone_number is E.164 and phone_number_local is 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.

BucketPer minuteApplies to
api-v2-read240Every authenticated GET, including polling for a code
api-v2-write60Cancel, skip, rental updates, webhook endpoint changes
api-v2-order30Creating and reusing orders, creating and extending rentals
api-v2-public30The 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

HeaderMeaning
X-Api-VersionThe contract version this response was produced under.
LocationOn every 201, the URL of the thing that was just created.
Retry-AfterOn a 429, and on the retryable 503s. Seconds to wait before trying again.
Cache-Controlno-store, private on everything that carries account data. Do not cache it.
VaryOrigin, 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

EventFires when
sms.receivedA 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.receivedA message arrived on a long-term rental.
rental.expiringA 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.

ScopeGrants
account:readRead your account and balance
services:readRead the service catalogue and pricing
orders:readRead orders and their messages
orders:writeCreate, cancel, skip and reuse orders
rentals:readRead rentals and their messages
rentals:writeCreate, extend and modify rentals
webhooks:readRead webhook endpoints
webhooks:writeCreate, 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

ParameterInTypeRequiredDescription
countryquerystringOptionalPrice 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.
searchquerystringOptionalCase-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

ParameterInTypeRequiredDescription
created_afterquerystring (date-time)OptionalOnly orders created at or after this instant. Any parseable date-time; send ISO 8601 with an offset to avoid ambiguity.
created_beforequerystring (date-time)OptionalOnly orders created at or before this instant.
cursorquerystringOptionalThe `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing.
limitqueryintegerOptionalRows per page. Defaults to 25, capped at 100.
service_idqueryintegerOptionalOnly orders for this service. See `GET /services`.
statusquerypending | completed | canceled | expiredOptionalOnly orders in this state.

POST/ordersorders:write

Order a one-time number

ParameterInTypeRequiredDescription
countrybodystringRequiredStorage 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_idbodyintegerRequiredFrom `GET /services`.
max_pricebodystring or numberOptionalA 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

ParameterInTypeRequiredDescription
orderpathintegerRequiredThe order id, as returned by `POST /orders`.

GET/orders/{order}/messagesorders:read

Read the messages on an order

ParameterInTypeRequiredDescription
orderpathintegerRequiredThe order id, as returned by `POST /orders`.

POST/orders/{order}/cancelorders:write

Cancel an order

ParameterInTypeRequiredDescription
orderpathintegerRequiredThe order id, as returned by `POST /orders`.

POST/orders/{order}/skiporders:write

Cancel an order and blacklist the number

ParameterInTypeRequiredDescription
orderpathintegerRequiredThe order id, as returned by `POST /orders`.

POST/orders/{order}/reuseorders:write

Order the same number again, at half price

ParameterInTypeRequiredDescription
orderpathintegerRequiredThe 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

ParameterInTypeRequiredDescription
cursorquerystringOptionalThe `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing.
limitqueryintegerOptionalRows per page. Defaults to 25, capped at 100.
statusqueryactive | expired | allOptionalWhich rentals to include. `active` means the expiry is in the future. Defaults to `all`.

POST/rentalsrentals:write

Rent a number

ParameterInTypeRequiredDescription
auto_renewbodybooleanRequiredRequired, not optional. Required rather than defaulted, because a recurring charge rides on it.
countrybodystringRequiredA country that can be rented. In practice only NL and UK carry for-hire stock.
packagebodystringRequiredA `key` from `GET /rental-packages`.

GET/rentals/{rental}rentals:read

Retrieve a rental

ParameterInTypeRequiredDescription
rentalpathintegerRequiredThe rental contract id, as returned by `POST /rentals`.

PATCH/rentals/{rental}rentals:write

Change renewal settings

ParameterInTypeRequiredDescription
rentalpathintegerRequiredThe rental contract id, as returned by `POST /rentals`.
auto_renewbodybooleanOptionalWhether to charge for another term when this one ends. Turning it off is how you cancel: the number stays yours until `expires_at`.
renewal_packagebodystringOptionalWhich 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

ParameterInTypeRequiredDescription
rentalpathintegerRequiredThe rental contract id, as returned by `POST /rentals`.

POST/rentals/{rental}/extendrentals:write

Add time to a rental

ParameterInTypeRequiredDescription
packagebodystringRequiredA `key` from `GET /rental-packages`. Its days are added to the current expiry, not to today. This does not change `renewal_package`.
rentalpathintegerRequiredThe 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

ParameterInTypeRequiredDescription
eventsbodyarray of sms.received | rental.sms.received | rental.expiringRequiredWhich 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.
urlbodystring (uri)RequiredHTTPS, 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.
descriptionbodystringOptionalFree-text label for your own bookkeeping. Never sent to the endpoint.
enabledbodybooleanOptionalRegister the endpoint switched off β€” useful for provisioning before the receiver is deployed.

GET/webhook-endpoints/{endpoint}webhooks:read

Retrieve a webhook endpoint

ParameterInTypeRequiredDescription
endpointpathintegerRequiredThe webhook endpoint id.

PATCH/webhook-endpoints/{endpoint}webhooks:write

Update a webhook endpoint

ParameterInTypeRequiredDescription
endpointpathintegerRequiredThe webhook endpoint id.
descriptionbodystringOptional
enabledbodybooleanOptionalFalse 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`.
eventsbodyarray of sms.received | rental.sms.received | rental.expiringOptional
urlbodystring (uri)OptionalValidated exactly as strictly as on create.

DELETE/webhook-endpoints/{endpoint}webhooks:write

Delete a webhook endpoint

ParameterInTypeRequiredDescription
endpointpathintegerRequiredThe webhook endpoint id.

GET/webhook-endpoints/{endpoint}/deliverieswebhooks:read

List recent deliveries to an endpoint

ParameterInTypeRequiredDescription
endpointpathintegerRequiredThe webhook endpoint id.
cursorquerystringOptionalThe `next_cursor` from a previous page. Opaque: do not construct, parse or persist it beyond the walk you are doing.
limitqueryintegerOptionalRows per page, clamped to 1-100 rather than rejected. Defaults to 25.

POST/webhook-endpoints/{endpoint}/testwebhooks:write

Send a test delivery

ParameterInTypeRequiredDescription
endpointpathintegerRequiredThe webhook endpoint id.

Countries

The list changes very rarely, so it lives here rather than behind an endpoint.

CodeCountryDiallingAvailable for
NLNetherlands+31One-time, Rentals
UKUnited Kingdom+44One-time, Rentals
USAUnited States+1One-time
PHPhilippines+63One-time
PLPoland+48One-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.