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)
ResponseRetry?
200No — success (trackingId returned)
400No — fix the request
401 / 403Yes — refresh token, retry once
429, 5xx, timeoutYes — 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 before FAILED.

Next steps


Related