Authentication

Every Oris API request requires HMAC-SHA256 signing. The SDKs handle this automatically. This page documents the underlying protocol for developers who make direct HTTP calls or build custom clients.

Using an SDK? The Python and TypeScript SDKs sign every request automatically. You only need this page if you are building a custom HTTP client or debugging signature failures.

Required Headers

Every request must include these five headers. Missing or invalid headers produce a 401 Authentication failed response with no diagnostic detail.

HeaderFormatDescription
Authorizationoris_sk_live_...Your API key. Identifies your developer account.
X-Request-Signature64-char hex stringHMAC-SHA256 of the canonical request string.
X-TimestampUnix epoch secondsCurrent time. Must be within 30 seconds of server time.
X-Nonce16-128 character stringUnique per request. Prevents replay attacks.
X-Agent-IDUUIDRequired for agent-scoped endpoints. Optional for developer-only endpoints.

POST and PATCH requests also include an Idempotency-Key header (UUID). The SDKs generate this automatically. For direct calls, generate a UUID v4 for each mutation request.

Canonical Request Format

The signature is computed over a canonical string that binds the timestamp, HTTP method, request path, and request body together. This prevents tampering with any component of the request.

Canonical String
{timestamp}.{METHOD}.{path}.{SHA-256(body)}
ComponentValueExample
timestampSame value sent in X-Timestamp1711234567
METHODUppercase HTTP methodPOST
pathFull path including /api/v1 prefix/api/v1/oris/payments/send
SHA-256(body)Hex-encoded SHA-256 of the request body. For GET requests with no body, hash the empty string.e3b0c44298fc...

Key Derivation

When you register as a developer, you receive an api_key and api_secret. The signing key is derived from the secret using SHA-256. The server stores the same hash and computes the HMAC with it. The raw api_secret is never stored on the server and cannot be recovered.

Key Derivation
# Client side (SDK) signing_key = SHA-256(api_secret) # Server side (stored in database) api_secret_hash = SHA-256(api_secret) # Same value # Both compute signature = HMAC-SHA256(signing_key, canonical_string)

Signature Computation

sign_request.py
import hashlib, hmac, time, uuid def sign_request(api_secret: str, method: str, path: str, body: bytes | None = None) -> dict: """Return the auth headers for an Oris API request.""" timestamp = str(int(time.time())) nonce = uuid.uuid4().hex # Derive signing key signing_key = hashlib.sha256(api_secret.encode()).hexdigest() # Build canonical string body_hash = hashlib.sha256(body or b"").hexdigest() canonical = f"{timestamp}.{method.upper()}.{path}.{body_hash}" # Compute HMAC-SHA256 signature = hmac.new( signing_key.encode(), canonical.encode(), hashlib.sha256 ).hexdigest() return { "Authorization": api_key, "X-Request-Signature": signature, "X-Timestamp": timestamp, "X-Nonce": nonce, "Idempotency-Key": str(uuid.uuid4()), }
signRequest.ts
import { createHmac, createHash, randomUUID } from 'crypto' function signRequest( apiKey: string, apiSecret: string, method: string, path: string, body?: string ): Record<string, string> { const timestamp = String(Math.floor(Date.now() / 1000)) const nonce = randomUUID() // Derive signing key const signingKey = createHash('sha256') .update(apiSecret).digest('hex') // Build canonical string const bodyHash = createHash('sha256') .update(body ?? '').digest('hex') const canonical = `${timestamp}.${method.toUpperCase()}.${path}.${bodyHash}` // Compute HMAC-SHA256 const signature = createHmac('sha256', signingKey) .update(canonical).digest('hex') return { 'Authorization': apiKey, 'X-Request-Signature': signature, 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'Idempotency-Key': randomUUID(), } }
Terminal
# Compute each component TIMESTAMP=$(date +%s) NONCE=$(uuidgen) BODY='{"agent_id":"550e8400-...","amount":12.50}' BODY_HASH=$(echo -n "$BODY" | sha256sum | cut -d' ' -f1) SIGNING_KEY=$(echo -n "$API_SECRET" | sha256sum | cut -d' ' -f1) CANONICAL="${TIMESTAMP}.POST./api/v1/oris/payments/send.${BODY_HASH}" SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | cut -d' ' -f2) # Send the request curl -X POST https://api.useoris.ai/api/v1/oris/payments/send \ -H "Authorization: $API_KEY" \ -H "X-Request-Signature: $SIGNATURE" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Nonce: $NONCE" \ -H "Content-Type: application/json" \ -d "$BODY"

Timestamp Tolerance

The server accepts requests with timestamps within a 30-second window of the current server time. Requests outside this window are rejected with 401. This prevents replay attacks while allowing for reasonable clock drift.

Clock synchronization. Ensure your system clock is synchronized with NTP. A clock skew greater than 30 seconds will cause all requests to fail authentication.

Nonce Handling

Every request must include a unique nonce in the X-Nonce header. The server stores each nonce in Redis with a 30-second TTL using SETNX. If the same nonce appears within that window, the request is rejected. This provides replay protection for the duration of the timestamp tolerance window.

ConstraintValue
Minimum length16 characters
Maximum length128 characters
TTL30 seconds
StorageRedis SETNX (fail-closed)

Verification Order

The server validates each request in strict sequential order. If any step fails, the request is immediately rejected with an opaque 401 error. No diagnostic information is returned to prevent information leakage.

StepCheckFailure
1Authorization header format (oris_sk_live_ prefix)401
2Timestamp within 30-second tolerance401
3Nonce uniqueness (Redis SETNX)401
4Developer lookup by API key prefix401
5HMAC-SHA256 signature verification (timing-safe)401
6Rate limit check (Lua token bucket)429

Fail-Closed Design

If Redis is unavailable for nonce validation or rate limiting, the request is rejected. The system never falls back to an unauthenticated or unprotected state. Financial transactions require all security checks to pass.

Opaque errors. The server returns "Authentication failed." for all 401 responses. It never discloses which specific check failed. This is intentional. If your requests are being rejected, verify each component (timestamp, nonce, signature) independently using the code examples on this page.

API Key Format

CredentialPrefixLengthStored On Server
API Keyoris_sk_live_12 + 43 chars (32 random bytes, URL-safe base64)SHA-256 hash + 12-char prefix
API Secretoris_ss_live_12 + 64 chars (48 random bytes, URL-safe base64)SHA-256 hash only

Both credentials are shown exactly once during developer registration. The API secret cannot be recovered. If you lose it, you must rotate your key pair using the Developers API.

Key Rotation

Call POST /oris/developers/rotate-key to generate a new key pair. The old key is invalidated immediately. All active sessions using the old key will fail on their next request.

Python
from oris import Agent agent = Agent(api_key="oris_sk_live_old...", api_secret="oris_ss_live_old...") new_keys = agent.rotate_key() print(new_keys.api_key) # "oris_sk_live_new..." print(new_keys.api_secret) # "oris_ss_live_new..." (save this immediately)