Authentication

One header. Server-side keys, class-tagged live vs test, scoped least-privilege — and no wallet signatures anywhere.

Drazill authentication is deliberately plain: an integration sends one API key in one header. There is no request-signing ceremony to get started, no wallet to connect, no EIP-712 payloads. (An optional per-key HMAC signature is available for high-assurance bots — see Request signing below — but it is opt-in, not the default.)

The X-API-Key header

$curl -H "X-API-Key: $DRAZILL_API_KEY" \
> https://staging.drazill.com/api/v1/auth/me

API keys are server-side secrets. The API’s CORS policy allows only Drazill’s own first-party web origins to send credentials, so a third-party browser app cannot call the API directly with a key — browser and mobile apps must proxy requests through a backend you control that holds the key. (This is enforced, not advisory: cross-origin credentialed requests from other origins are rejected. Source: docs/api/cors-and-auth.md.)

Bearer tokens are Drazill’s own session scheme

The API also accepts Authorization: Bearer <jwt> — but those are first-party session tokens minted by the Drazill web/mobile apps for a signed-in user, not a credential you provision for an integration. Build integrations with API keys. Bearer auth is documented here only so you recognize it in the reference.

Key classes: live vs test

Every key is class-tagged by its prefix, set at creation (environment: "live" | "test"):

PrefixClassBehaviour
drzl_live_LiveReaches the real-money API on production.
drzl_test_Test (paper)Routed onto the price-isolated paper engine; hard-rejected with TEST_KEY_LIVE_ENDPOINT on any real-money endpoint. Requires the sandbox (DEVELOPER_SANDBOX_ENABLED) — on by default on staging, off on production.

Never paste a drzl_live_ key into a browser, a notebook you share, or any client you do not control. Full details in Paper vs live and Sandbox. Source: app/schemas/api_key.py, app/models/api_key.py.

Scopes

Keys carry least-privilege scopes. A new key defaults to read-only scopes; write scopes must be requested explicitly and acknowledged (acknowledge_write_scopes: true), and they require a verified email on the owning account (ensure_write_scopes_email_verified, app/api/routes/api_keys.py). Admin scopes are never grantable to a user-created key — a leaked or self-minted key can never perform admin actions (validate_user_creatable_scopes, app/schemas/api_key.py).

ScopeClassGrants
markets:readReadRead-only access to markets.
orders:readReadRead-only access to orders.
wallet:readReadRead-only access to wallet.
users:readReadRead-only access to users.
comments:readReadRead-only access to comments.
watchlist:readReadRead-only access to watchlist.
notifications:readReadRead-only access to notifications.
webhooks:readReadRead-only access to webhooks.
sports:readReadRead sports market data.
crypto:readReadRead crypto market data.
data:readReadRead the historical-dataset export and usage APIs.
websocketReadOpen a realtime WebSocket connection.
orders:writeWriteCan place and cancel trades.
markets:writeWriteCan create or mutate markets if the owner has creator privileges.
wallet:writeWriteCan initiate wallet mutations.
users:writeWriteCan modify profile data.
comments:writeWriteCan post or modify comments.
watchlist:writeWriteCan mutate watchlists.
notifications:writeWriteCan mutate notification state.
webhooks:writeWriteCan create, rotate, disable, and replay developer webhooks.
env:paperSandboxSandbox key — real-money actions are blocked; use the paper-trading endpoints. Ideal for testing bots against staging.
admin:readAdminCan read administrative data if the owner is an admin.
admin:writeAdminCan mutate administrative data if the owner has admin privileges; privileged role changes still require super-admin session approval.

The env:paper scope marks a key as sandbox/paper-only; websocket is required to authenticate the realtime feed. This table is generated from the scope constants in app/schemas/api_key.py and drift-gated in CI — it cannot fall out of sync with the code.

Key lifecycle

  • CreatePOST /api/v1/api-keys with a name, environment, optional scopes, and optional expires_in_days (1–365; omit for no expiry). The full key is returned once and never again — store it immediately.
  • Rotate — issue a new secret for a key while keeping its identity. Rotation dual-signs webhooks during a grace window so you can roll secrets without missing deliveries (see Webhooks).
  • Revoke — disable a key immediately.

You receive a security email on every create / rotate / revoke — showing the key prefix only, never the raw secret (docs/runbooks/developer-platform.md).

Request signing

A key created with signing_required: true must HMAC-sign every request. The signing secret (prefix drzl_sign_) is returned once at creation and is never sent over the wire. Sign the string "<unix_ts>.<METHOD>.<path>." + body with the secret and attach two headers:

  • X-Drazill-Timestamp: <unix seconds>
  • X-Drazill-Signature: v1=<hex hmac-sha256>

Requests outside a ±300-second skew window are rejected. This is a leak-resistant second factor for bots: even a stolen key is useless without the separate signing secret. Source: app/services/api_signing.py.

Signing helper (Python)
1import hashlib
2import hmac
3import time
4
5
6def drazill_signature(secret: str, method: str, path: str, body: bytes = b"") -> dict:
7 ts = int(time.time())
8 signed = f"{ts}.{method.upper()}.{path}.".encode() + body
9 digest = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
10 return {"X-Drazill-Timestamp": str(ts), "X-Drazill-Signature": f"v1={digest}"}

Withdrawals require an MFA step-up

Initiating a withdrawal requires a fresh MFA step-up: call POST /api/v1/security/mfa/step-up, whose proof is cached for 300 seconds (MFA_STEP_UP_TTL_SECONDS). Within that window the withdrawal proceeds; after it, the step-up must be repeated. Source: app/api/routes/security.py, app/core/config.py.

Developer terms

PENDING LEGAL REVIEW. The developer API terms (docs/legal/api-terms.md, version 2026-07-17) are a good-faith engineering draft that has not been reviewed or approved by legal counsel and is not a binding agreement. It is mirrored here with that status, exactly as the source carries it.

Terms acceptance is gated by DEVELOPER_TERMS_REQUIRED, which is false today — key creation does not currently require recorded acceptance. When the owner enables it, creating a key will require a current acceptance of API_TERMS_VERSION (2026-07-17), re-requested whenever the version changes. This page states the real flag state rather than implying a gate that is off. Source: app/core/config.py, docs/legal/api-terms.md.

Authenticating the WebSocket

The realtime feed authenticates via the first message on the socket (not a header or query param), accepting either credential:

First WebSocket message
1{ "type": "auth", "api_key": "drzl_test_..." }
…or a session token
1{ "type": "auth", "token": "<jwt>" }

Source: app/api/routes/websocket.py. The full realtime auth flow is under Realtime.