Production auth & retries
Cache tokens, recover from 401/403, and back off cleanly.
Cache and refresh OAuth tokens, recover from 401/403, and back off cleanly so your integration stays reliable under load.
Prerequisite: Send your first transactional email and Get authenticated.
1. Get a token
Exchange your Connect application credentials (HTTP Basic) for a Bearer token:
curl -s -X POST 'https://auth.routee.net/oauth/token' \
-u 'YOUR_APPLICATION_ID:YOUR_APPLICATION_SECRET' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&scope=transactional_email'The response contains access_token and expires_in (3600 seconds = 1 hour).
2. Cache the token, refresh before expiry
Don't request a token per message — you'll add latency and waste calls. Cache it and refresh before expires_in elapses (for example, at 90% of its lifetime).
import time, requests
_cache = {"token": None, "expires_at": 0}
def get_token(app_id, secret):
now = time.time()
if _cache["token"] and now < _cache["expires_at"]:
return _cache["token"]
resp = requests.post(
"https://auth.routee.net/oauth/token",
auth=(app_id, secret),
data={"grant_type": "client_credentials", "scope": "transactional_email"},
timeout=10,
)
resp.raise_for_status()
body = resp.json()
_cache["token"] = body["access_token"]
_cache["expires_at"] = now + body["expires_in"] * 0.9 # refresh early
return _cache["token"]
Share one cached token across workers (for example, in Redis) so a fleet of senders doesn't each mint its own.
3. Handle 401 / 403 — refresh and retry once
An expired or revoked token returns 401 or 403. Discard the cached token, fetch a fresh one, and retry the request once.
def send_email(payload, app_id, secret):
token = get_token(app_id, secret)
resp = requests.post(
"https://connect.routee.net/transactional-email",
headers={"Authorization": f"Bearer {token}"},
json=payload, timeout=15,
)
if resp.status_code in (401, 403):
_cache["token"] = None # force refresh
token = get_token(app_id, secret)
resp = requests.post(
"https://connect.routee.net/transactional-email",
headers={"Authorization": f"Bearer {token}"},
json=payload, timeout=15,
)
resp.raise_for_status()
return resp.json() # { "trackingId": "..." }4. Back off on transient errors
For timeouts, 429, and 5xx, retry with exponential backoff and jitter. Do not retry 400 responses — they're permanent request errors (see Email error codes).
import random, time
def with_backoff(fn, max_attempts=5, base=0.5, cap=30):
for attempt in range(max_attempts):
try:
return fn()
except Transient:
if attempt == max_attempts - 1:
raise
delay = min(cap, base * 2 ** attempt) + random.uniform(0, base)
time.sleep(delay)| Response | Retry? |
|---|---|
200 | No — success (trackingId returned) |
400 | No — fix the request |
401 / 403 | Yes — refresh token, retry once |
429, 5xx, timeout | Yes — exponential backoff |
5. Control delivery retries server-side
Separate from your client retries, Routee retries delivery according to the send payload:
{
"ttl": 120,
"maxAttempts": 5
}ttl— minutes to keep attempting delivery.maxAttempts— maximum delivery attempts beforeFAILED.
Next steps
- Build & secure a webhook receiver — get delivery results pushed to you
- Handle bounces & interpret statuses — react to what happens after send
- Send a transactional email — full API reference

