A client worth keeping
Base URL https://api.affix-io.com, an API key in Authorization: Bearer or X-API-Key, JSON in and JSON out. The three things worth building in from the start are an idempotency key on writes, a 429 handler that respects Retry-After, and capturing X-Request-Id into your own logs so a support conversation has a shared reference.
import os
import time
import uuid
from typing import Any
import httpx
BASE = os.environ.get("AFFIX_API_BASE", "https://api.affix-io.com")
class AffixError(RuntimeError):
def __init__(self, status: int, payload: dict[str, Any], request_id: str | None):
self.status = status
self.payload = payload
self.request_id = request_id
super().__init__(f"{status} {payload.get('error', 'error')}: {payload.get('message', '')}")
class Affix:
def __init__(self, api_key: str, base: str = BASE, timeout: float = 30.0):
self._client = httpx.Client(
base_url=base,
timeout=timeout,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"User-Agent": "affix-python-example/1.0",
},
)
def close(self) -> None:
self._client.close()
def post(self, path: str, body: dict[str, Any], *, idempotency_key: str | None = None,
attempts: int = 3) -> dict[str, Any]:
headers = {"Idempotency-Key": idempotency_key or str(uuid.uuid4())}
for attempt in range(1, attempts + 1):
response = self._client.post(path, json=body, headers=headers)
if response.status_code == 429 and attempt < attempts:
time.sleep(float(response.headers.get("Retry-After", "1")))
continue
payload = response.json()
if response.status_code >= 400:
raise AffixError(response.status_code, payload, response.headers.get("X-Request-Id"))
return payload
raise AffixError(429, {"error": "rate_limited"}, None)
def verify(self, body: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
return self.post("/v1/verify", body, **kwargs)
def gate(self, body: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
return self.post("/v1/gate/verify", body, **kwargs)
Reuse one Affix instance for the life of the process. A new httpx.Client per call throws away connection reuse, and at ten requests per second per key that shows up quickly. requests.Session works the same way if you would rather not add httpx.
On the idempotency key
Send one on every prove and verify call and keep it stable across retries of the same logical operation. A replay inside 24 hours returns the original 2xx body with Idempotency-Replayed: true, which is what makes a retry safe after a timeout you cannot interpret.