Security & Trust Model
No marketing language here. Just the mechanics.
Authentication: HMAC-SHA256
Every API request must be signed. We don't use bearer tokens or session cookies.
Required Headers
| Header | Description |
|---|---|
X-Api-Key | Your integrator API key |
X-Timestamp | Unix timestamp (seconds) |
X-Nonce | Unique string per request |
X-Signature | HMAC-SHA256 signature |
Signature Calculation
javascript
// 1. Build the signature base string
signatureBase = method + "\n" +
path + "\n" +
timestamp + "\n" +
nonce + "\n" +
bodyHash
// 2. Hash the request body (SHA-256, hex-encoded)
bodyHash = sha256(requestBody || "")
// 3. Compute HMAC-SHA256
signature = hmac_sha256(signatureBase, apiSecret)
// Example
GET /v1/escrows/esc_123
X-Timestamp: 1735430400
X-Nonce: unique_abc123
signatureBase = "GET\n/v1/escrows/esc_123\n1735430400\nunique_abc123\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" Implementation Example (TypeScript)
typescript
import { createHmac, createHash } from 'crypto';
function signRequest(
method: string,
path: string,
body: string | null,
apiSecret: string
): { timestamp: string; nonce: string; signature: string } {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyHash = createHash('sha256')
.update(body || '')
.digest('hex');
const signatureBase = [method, path, timestamp, nonce, bodyHash].join('\n');
const signature = createHmac('sha256', apiSecret)
.update(signatureBase)
.digest('hex');
return { timestamp, nonce, signature };
} Replay Protection
Timestamp Validation
Requests with timestamps older than 300 seconds (5 minutes) are rejected.
javascript
// Server-side check
if (Math.abs(serverTime - requestTimestamp) > 300) {
return 401; // TIMESTAMP_EXPIRED
} Nonce Tracking
Each X-Nonce value can only be used once within the timestamp window. Reusing a nonce
returns 401 NONCE_REUSED.
Idempotency
All POST requests require an Idempotency-Key header.
| Scenario | Result |
|---|---|
| New key + new request | Processed normally |
| Same key + same payload | Returns cached response |
| Same key + different payload | 409 IDEMPOTENCY_MISMATCH |
Key Requirements
- Format: UUID v4 recommended
- Retention: 24 hours
- Scope: Per integrator
http
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Webhook Security
Outbound webhooks are HMAC-signed using the same scheme.
Verifying Webhook Signatures
javascript
// Incoming webhook
POST /your-webhook-endpoint
X-Clearhold-Timestamp: 1735430400
X-Clearhold-Signature: abc123...
// Verify
signatureBase = "POST\n/your-webhook-endpoint\n" + timestamp + "\n" + bodyHash
expectedSignature = hmac_sha256(signatureBase, webhookSecret)
if (!timingSafeEqual(expectedSignature, receivedSignature)) {
return 401; // Signature mismatch
} Retry Policy
| Attempt | Delay |
|---|---|
| 1 | 0s (immediate) |
| 2 | 1s |
| 3 | 2s |
| 4 | 4s |
| 5 | 8s |
| 6 | 16s (final) |
After 6 failed attempts, webhooks are dead-lettered. No automatic recovery. You'll need to manually replay from audit log.
Rate Limits
| Tier | Limit |
|---|---|
| Standard | 100 requests/minute |
| Pro | 1,000 requests/minute |
| Enterprise | Custom |
Rate limit headers included in every response:
http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1735430460 Security Guarantees
- ✓ All data encrypted in transit (TLS 1.3)
- ✓ All data encrypted at rest (AES-256)
- ✓ Timing-safe signature comparison
- ✓ No sensitive data in logs
- ✓ Audit trail for all state transitions