DOCS

OAuth 2.0 authentication

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:

  1. Generate an RSA key pair and register your public key with Zonos.
  2. At runtime, sign a JWT assertion with your private key and POST it to the token endpoint.
  3. Zonos returns a short-lived access token.
  4. Include the access token as Authorization: Bearer <token> on every API request.

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 

1# Generate private key
2openssl genrsa -out private_key.pem 4096
3 
4# Extract public key
5openssl rsa -in private_key.pem -pubout -out public_key.pem

Share 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 

ClaimValue
issYour Zonos Organization ID (e.g. "org_abc123")
subIdentifies the calling service (e.g. "checkout-service")
audMust be exactly "zonos-auth"
expUnix timestamp; 60–300 seconds from iat
iatUnix timestamp of issuance
jtiUnique UUID per assertion (enables replay detection)

The JWT header must specify "alg": "RS256" and "typ": "JWT".

Code examples 

1import jwt, uuid, time
2 
3with open("private_key.pem") as f:
4 private_key = f.read()
5 
6now = int(time.time())
7assertion = jwt.encode(
8 {
9 "iss": "org_abc123",
10 "sub": "checkout-service",
11 "aud": "zonos-auth",
12 "iat": now,
13 "exp": now + 300,
14 "jti": str(uuid.uuid4()),
15 },
16 private_key,
17 algorithm="RS256",
18)

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 

FieldRequiredValue
grant_typeYes"urn:ietf:params:oauth:grant-type:jwt-bearer"
assertionYesYour signed JWT (compact serialization)

Request and response 

1{
2 "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
3 "assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
4}
Response fieldDescription
access_tokenBearer token for all subsequent API requests
token_typeAlways "Bearer"
expires_inSeconds until expiry (default: 300)
scopeSpace-separated permissions granted to this token

Full code examples 

1import requests
2 
3response = requests.post(
4 "https://api.zonos.com/oauth/token",
5 json={
6 "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
7 "assertion": assertion,
8 },
9)
10data = response.json()
11access_token = data["access_token"]
12expires_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 

1curl -X POST https://api.zonos.com/graphql \
2 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
3 -H "Content-Type: application/json" \
4 -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.

1import time, requests
2 
3_cache = {"access_token": None, "expires_at": 0}
4 
5def get_access_token():
6 if time.time() < _cache["expires_at"] - 30:
7 return _cache["access_token"]
8 
9 assertion = build_jwt_assertion()
10 data = requests.post(
11 "https://api.zonos.com/oauth/token",
12 json={
13 "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
14 "assertion": assertion,
15 },
16 ).json()
17 
18 _cache["access_token"] = data["access_token"]
19 _cache["expires_at"] = time.time() + data["expires_in"]
20 return _cache["access_token"]

Error reference 

All errors follow the OAuth 2.0 error response format (RFC 6749 §5.2):

1{
2 "error": "invalid_grant",
3 "error_description": "JWT assertion has expired"
4}
HTTP StatuserrorCause
400unsupported_grant_typegrant_type was not urn:ietf:params:oauth:grant-type:jwt-bearer
400invalid_requestMissing or malformed field
401invalid_grantInvalid signature, expired assertion, unknown org, or unregistered key
500server_errorInternal error — contact Zonos support if persistent

Common invalid_grant causes:

  • exp is in the past — ensure your system clock is NTP-synchronized
  • aud is not exactly "zonos-auth"
  • iss does 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_token or assertion values. Treat both as credentials.
Book a demo

Was this page helpful?