Step 1 — Register your public key (one-time setup)
Generate a 4096-bit RSA key pair and share the public key with Zonos. This is done once during onboarding.
Generate the key pair
# Generate private keyopenssl genrsa -out private_key.pem 4096 # Extract public keyopenssl rsa -in private_key.pem -pubout -out public_key.pemShare public_key.pem with Zonos. Store private_key.pem in a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) — never in source control or environment variables.
Zonos will register your key and return your Organization ID, which becomes the iss claim in all JWT assertions.
Step 2 — Build a JWT assertion
Sign a JWT with RS256 using your private key. The assertion is valid for a single token exchange — keep the expiry window short (60–300 seconds).
Required claims
| Claim↕ | Value↕ |
|---|---|
iss | Your Zonos Organization ID (e.g. "org_abc123") |
sub | Identifies the calling service (e.g. "checkout-service") |
aud | Must be exactly "zonos-auth" |
exp | Unix timestamp; 60–300 seconds from iat |
iat | Unix timestamp of issuance |
jti | Unique UUID per assertion (enables replay detection) |
The JWT header must specify "alg": "RS256" and "typ": "JWT".
Code examples
import jwt, uuid, time with open("private_key.pem") as f: private_key = f.read() now = int(time.time())assertion = jwt.encode( { "iss": "org_abc123", "sub": "checkout-service", "aud": "zonos-auth", "iat": now, "exp": now + 300, "jti": str(uuid.uuid4()), }, private_key, algorithm="RS256",)Step 3 — Exchange the assertion for an access token
Send the signed JWT to the Zonos token endpoint to receive a short-lived Bearer token.
Endpoint
POST https://api.zonos.com/oauth/token
Content-Type: application/json
application/x-www-form-urlencoded is also accepted.
Request fields
| Field↕ | Required↕ | Value↕ |
|---|---|---|
grant_type | Yes | "urn:ietf:params:oauth:grant-type:jwt-bearer" |
assertion | Yes | Your signed JWT (compact serialization) |
Request and response
{ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}| Response field↕ | Description↕ |
|---|---|
access_token | Bearer token for all subsequent API requests |
token_type | Always "Bearer" |
expires_in | Seconds until expiry (default: 300) |
scope | Space-separated permissions granted to this token |
Full code examples
import requests response = requests.post( "https://api.zonos.com/oauth/token", json={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": assertion, },)data = response.json()access_token = data["access_token"]expires_in = data["expires_in"]Step 4 — Call Zonos APIs with the access token
Include the access token as a Bearer token in the Authorization header on every Zonos API request.
Example request
curl -X POST https://api.zonos.com/graphql \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "query": "{ ... }" }'Token lifecycle and caching
Access tokens expire in 5 minutes by default. Cache the token and refresh proactively — do not request a new token on every API call. Each refresh requires a newly signed JWT assertion.
import time, requests _cache = {"access_token": None, "expires_at": 0} def get_access_token(): if time.time() < _cache["expires_at"] - 30: return _cache["access_token"] assertion = build_jwt_assertion() data = requests.post( "https://api.zonos.com/oauth/token", json={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": assertion, }, ).json() _cache["access_token"] = data["access_token"] _cache["expires_at"] = time.time() + data["expires_in"] return _cache["access_token"]Error reference
All errors follow the OAuth 2.0 error response format (RFC 6749 §5.2):
{ "error": "invalid_grant", "error_description": "JWT assertion has expired"}| HTTP Status↕ | error↕ | Cause↕ |
|---|---|---|
400 | unsupported_grant_type | grant_type was not urn:ietf:params:oauth:grant-type:jwt-bearer |
400 | invalid_request | Missing or malformed field |
401 | invalid_grant | Invalid signature, expired assertion, unknown org, or unregistered key |
500 | server_error | Internal error — contact Zonos support if persistent |
Common invalid_grant causes:
expis in the past — ensure your system clock is NTP-synchronizedaudis not exactly"zonos-auth"issdoes not match your registered Organization ID- Public key was rotated but not yet updated with Zonos
Security best practices
- Protect your private key. Store it in a dedicated secrets manager — never in source control, environment variables, or logs.
- Keep assertions short-lived. 60–300 seconds is standard; there is no reason to issue longer ones.
- Include
jti. A unique value per assertion enables server-side replay detection. - Rotate key pairs periodically. Register a new public key with Zonos before revoking the old one to avoid downtime.
- Never log
access_tokenorassertionvalues. Treat both as credentials.
OAuth 2.0 authentication
Authenticate your backend services with Zonos using asymmetric key cryptography — no shared secrets.
Zonos supports machine-to-machine authentication via OAuth 2.0 JWT Bearer Token Grant (RFC 7523). Your service signs a short-lived JWT with your RSA private key; Zonos verifies it using your registered public key and returns a Bearer token scoped to your organization.
Flow summary:
Authorization: Bearer <token>on every API request.