Authentication and authorization

Cleared supports browser-based OAuth for hosted MCP clients and the native Cleared CLI, HMAC-SHA256 API-key authentication for backend integrations, and Privy JWT authentication for signed-in browser users. API keys contain a public identifier beginning clr_pk_ and a secret beginning clr_sk_. Keep the secret server-side. Invoice access tokens beginning clr_tk_ are a separate credential for viewing encrypted invoice metadata.

Hosted MCP OAuth#

Connect to https://api.clearedpay.xyz/mcp using Streamable HTTP and the client's OAuth flow. Cleared supports authorization code with S256 PKCE and dynamic client registration. Sign in through the browser, select an organization and network, and explicitly approve permissions. No API key or local server is required.

Hosted credentials are accepted only at /mcp, not as REST /v1 bearer credentials. Every call checks the connection, current membership, role, and approved scopes. Writes are opt-in; invoice writes additionally require consent-time limits and an active owner-activated signer. These checks are separate from legacy HMAC scope enforcement. See setup, permissions, and revocation.

HMAC wire format#

Send all four headers on every authenticated request, including GET:

HeaderValue
x-cleared-keyFull clr_pk_... key ID
x-cleared-ts13-digit Unix-millisecond timestamp
x-cleared-nonceFresh unique nonce, for example a UUID
AuthorizationCLEARED-HMAC followed by a standard base64 signature

Remove the clr_sk_ prefix and base64url-decode the remaining secret to its 32 bytes. Do not sign with the printable secret as UTF-8 text. Compute SHA-256 over the exact transmitted body bytes, using an empty byte sequence for a bodyless GET, and encode that hash as standard base64.

The signed message is these six values joined with \n, with no final newline:

text
UPPERCASE_METHOD
/v1/full/path
raw=query&in=transmitted_order
base64_sha256_of_raw_body
unix_milliseconds
nonce

Use an empty third line when there is no query. The query excludes ?; the path excludes the origin and query. Do not sort, re-encode, or canonicalize the query. Whitespace and property order in a JSON body matter because the signature covers bytes. Compute HMAC-SHA256 of this message and encode the result as standard base64, not base64url.

The default timestamp tolerance is five minutes and can be deployment-configured. Synchronize your clock. A nonce cannot be reused for the same key, even on reads. Retries need a new timestamp, nonce, and signature while retaining the operation's idempotency key and payload.

Executable, read-only example#

Save as read-invoices.mjs and run with Node.js 20+ or Bun. Supply CLEARED_KEY_ID and CLEARED_API_KEY through your environment or secret manager. CLEARED_API_URL optionally selects an API origin. This example only lists invoices; it does not issue, cancel, pay, or withdraw.

javascript
import { createHash, createHmac, randomUUID } from 'node:crypto';

const { CLEARED_API_KEY, CLEARED_KEY_ID } = process.env;
if (!CLEARED_API_KEY?.startsWith('clr_sk_') || !CLEARED_KEY_ID?.startsWith('clr_pk_')) {
  throw new Error('Set CLEARED_API_KEY and CLEARED_KEY_ID');
}
const secret = Buffer.from(CLEARED_API_KEY.slice('clr_sk_'.length), 'base64url');
if (secret.length !== 32) throw new Error('Expected a 32-byte HMAC secret');

const origin = new URL(process.env.CLEARED_API_URL ?? 'https://api.clearedpay.xyz');
if (
  origin.protocol !== 'https:' ||
  origin.pathname !== '/' ||
  origin.search ||
  origin.hash ||
  origin.username ||
  origin.password
) {
  throw new Error('Use an HTTPS API origin without a path or credentials');
}
const path = '/v1/invoices';
const query = 'limit=5&status=Issued';
const ts = String(Date.now());
const nonce = randomUUID();
const bodyHash = createHash('sha256').update(Buffer.alloc(0)).digest('base64');
const message = ['GET', path, query, bodyHash, ts, nonce].join('\n');
const signature = createHmac('sha256', secret).update(message).digest('base64');

const response = await fetch(`${origin.origin}${path}?${query}`, {
  method: 'GET',
  redirect: 'error',
  headers: {
    Accept: 'application/json',
    'x-cleared-key': CLEARED_KEY_ID,
    'x-cleared-ts': ts,
    'x-cleared-nonce': nonce,
    Authorization: `CLEARED-HMAC ${signature}`,
  },
  signal: AbortSignal.timeout(15000),
});
if (!response.ok) throw new Error(`API read failed: HTTP ${response.status}`);
const result = await response.json();
if (!Array.isArray(result.data?.items)) throw new Error('Unexpected invoice list');
console.log(result.data.items.map(({ id, status, amount }) => ({ id, status, amount })));
console.log({ page: result.data.page });

For JSON mutations, serialize the body once, hash Buffer.from(bodyString, 'utf8'), then send that same string with Content-Type: application/json. The request examples in invoices and customers are mutations, not read-only checks.

JWT authentication#

Use Authorization: Bearer <Privy access token> for user routes. Obtain the token through the signed-in Privy session; Cleared API-key secrets cannot be substituted. User authentication resolves organization ownership separately from API scopes. Do not include x-cleared-key with a JWT: mixed-auth routes prioritize HMAC, while key/agent/signer management explicitly rejects the API-key header.

CLI OAuth bearer authentication#

The native CLI obtains opaque access tokens beginning clr_oat_ through browser consent and S256 PKCE. These are not Privy JWTs, API keys, or invoice share tokens. Cleared binds each grant to one user, organization, client, audience, scope set, and exact API/web origins. Access tokens expire after 15 minutes; refresh tokens rotate and expire within 30 days. Do not build a custom integration around the CLI's stored bearer credential—use HMAC for unattended clients.

OAuth is accepted only on explicitly scoped CLI routes. It does not inherit every legacy route that accepts requireAuth, cannot manage keys/signers/agents, and cannot use browser prepare/submit routes. invoice:write permits the programmatic issue/cancel endpoints only when the existing active server signer and on-chain policy also allow them.

Actual authorization matrix#

All paths below are relative to /v1. A dash in the scope column means no explicit granular scope guard on that route, not unrestricted anonymous access. Ownership checks and transaction signatures still apply.

RoutesAccepted authenticationProgrammatic scope
GET /invoices, GET /invoices/:id, GET /invoices/lookup, GET /invoices/:id/shareHMAC, JWT, or CLI OAuth; studio ownershipinvoice:read
GET /invoices/:id/pdfHMAC or JWT, studio ownershipNone
POST /invoices/issue, POST /invoices/:id/cancel/submitHMAC or CLI OAuth, active write access and server signerinvoice:write
POST /invoices/:id/send-emailHMAC, JWT, or CLI OAuth; ownershipinvoice:write
POST /invoices/prepare, POST /invoices, POST /invoices/:id/cancel/prepare, POST /invoices/:id/cancelHMAC or JWT; submit requires signed UserOpNone
Invoice share/token reads and mutationsHMAC or JWT, merchant ownershipNone
GET /customers, GET /customers/:idHMAC, JWT, or CLI OAuth; studio contextcustomer:read
POST /customers, PATCH /customers/:id, DELETE /customers/:idHMAC, JWT, or CLI OAuth; studio contextcustomer:write
GET /studios/me, /studios/me/balance, /studios/me/invoices/statsHMAC, JWT, or CLI OAuthdashboard:read
GET /payments, GET /payments/export, /withdrawals routes, GET /accounts/:address/metricsHMAC or JWTNone
Other studio routes, API keys, signer, agents, Safe upgrade, /authPrivy JWT, appropriate user/owner checksAPI keys are not supported
/pay routes, GET /protocol/stats, POST /leadsNo user/API-key auth; payments still require payer signaturesNot applicable
POST /webhooks/resendProvider webhook signatureNot applicable

invoice:write implies invoice:read; customer:write implies customer:read. Legacy read satisfies the three granular read checks. Legacy write satisfies both granular write checks and all three reads. Legacy sponsor has no granular equivalent. These relationships apply only where a route checks a scope.

Granular HMAC scopes are not a complete least-privilege boundary across the legacy API. In particular, a read-labeled API key can reach legacy share/token mutations. Use keys only in trusted backends, apply an endpoint allowlist in your integration, and do not give a key to an untrusted party on the assumption that its scope makes every other operation inaccessible. This gap does not apply to hosted MCP OAuth or the CLI's separately allowlisted OAuth routes. Track the scope-enforcement gap.

This gap does not let an API key sign owner withdrawals. HMAC authenticates the HTTP request; withdrawal execution still requires a valid Safe-authorized signature. The invoice server signer is restricted to invoice operations. See security and withdrawal signing.