Skip to main content

Authentication

White Rabbit uses API keys to authenticate requests. The TypeScript SDK handles all signing automatically — you only need to provide your key.


API keys​

All SDK endpoints use a single Workspace API key (ws_...). Create and manage keys in the dashboard under Settings → API Keys.

WorkspaceClient​

Use a Workspace API key with WorkspaceClient to execute any component:

import { WorkspaceClient } from 'caller-sdk';

const workspace = new WorkspaceClient({
apiKey: process.env.WR_API_KEY!, // ws_...
});

WorkflowClient​

Use a Workspace API key and a workflow UUID with WorkflowClient to trigger and inspect workflow runs:

import { WorkflowClient } from 'caller-sdk';

const workflow = new WorkflowClient({
apiKey: process.env.WR_API_KEY!, // ws_...
workflowId: process.env.WR_WORKFLOW_ID!, // UUID of the workflow
});

Get it: Dashboard → Workspace Settings → API Keys → Create


Scoping keys to specific workflows​

label and allowedWorkflowIds are both required when creating a key. Pass an empty array for allowedWorkflowIds to allow all workflows; provide specific IDs to restrict the key to those workflows only.

// Unrestricted key — can trigger any workflow in the workspace
await client.workspaceApiKeys.create({
label: 'Development',
allowedWorkflowIds: [],
});

// Key scoped to two workflows only
await client.workspaceApiKeys.create({
label: 'Production trigger',
allowedWorkflowIds: [
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
],
});

Requests that target a workflow not in the allowedWorkflowIds list are rejected with 403 Forbidden.


Key security​

PracticeWhy
Store keys in environment variablesNever hardcode ws_ keys in source code
Use restricted keys for automationSet allowedWorkflowIds to limit blast radius if a key is exposed
Rotate immediately on exposureDashboard → Settings → API Keys → Revoke

Callback signature verification​

When you set callbackSecret on an execution, White Rabbit signs the callback body with HMAC-SHA256 and sends the signature in the X-WR-Signature header as hmac-sha256-v1=<hex>.

Node.js​

import { createHmac, timingSafeEqual } from 'crypto';

function verifyCallback(
rawBody: Buffer,
signatureHeader: string,
secret: string,
): boolean {
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader.replace('hmac-sha256-v1=', '');
return timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
);
}

// Express (with raw body parser)
app.post('/webhooks/wr', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-wr-signature'] as string;
if (!verifyCallback(req.body, sig, process.env.WR_CALLBACK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const execution = JSON.parse(req.body.toString());
console.log(execution.id, execution.status, execution.output);
res.sendStatus(200);
});
warning

Always use timingSafeEqual — standard string comparison is vulnerable to timing attacks.

Python​

import hmac, hashlib

def verify_callback(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
received = signature_header.replace('hmac-sha256-v1=', '')
return hmac.compare_digest(expected, received)

Rate limits​

Rate limits are enforced per workspace and scale with your plan.

PlanSoft limit (req/s)Hard limit (req/h)
Starter59,000
Growth1018,000
Professional2545,000
EnterpriseCustomCustom

POST /v1/sdk/components applies a 2× multiplier — it counts as 2 requests per call. All other endpoints count as 1.

When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header.


Advanced: REST request signing​

TypeScript SDK handles this for you

If you use WorkspaceClient or WorkflowClient, request signing is automatic. This section is only needed for custom REST clients or direct HTTP calls.

Every direct REST request requires three headers:

HeaderValue
X-Api-KeyYour Workspace API key (ws_...)
X-Sdk-TimestampUnix epoch in seconds
X-Sdk-SignatureBase64 Ed25519 signature

Signature format​

MESSAGE = METHOD + "|" + PATH + "|" + TIMESTAMP + "|" + JSON.stringify(body)

Empty bodies are included as "{}". The path is everything after the domain (e.g. /v1/sdk/components).

Your API secret​

When you create an API key in Dashboard → Workspace Settings → API Keys, the response includes an apiSecret field — the Ed25519 private key, DER-encoded and base64-encoded. Store it in WR_API_SECRET.

Keep your API secret secret

The apiSecret is the only credential that proves your identity for request signing. It is shown once at key creation time. If it is exposed, revoke the key immediately from the dashboard.

Node.js​

import { createPrivateKey, sign } from 'crypto';

const signingKey = createPrivateKey({
key: Buffer.from(process.env.WR_API_SECRET!, 'base64'),
format: 'der',
type: 'pkcs8',
});

function signRequest(
method: string,
path: string,
body: object = {},
): { timestamp: number; signature: string } {
const timestamp = Math.floor(Date.now() / 1000);
const message = [method.toUpperCase(), path, timestamp, JSON.stringify(body)].join('|');
const sig = sign(null, Buffer.from(message, 'utf-8'), signingKey);
return { timestamp, signature: sig.toString('base64') };
}

// Usage
const body = { module: 'RANDOM_UUID', input: {}, config: {}, waitForMs: 5000 };
const { timestamp, signature } = signRequest('POST', '/v1/sdk/components', body);

const res = await fetch('https://api.whiterabbit.app/v1/sdk/components', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.WR_API_KEY!,
'X-Sdk-Timestamp': String(timestamp),
'X-Sdk-Signature': signature,
},
body: JSON.stringify(body),
});

Python​

import base64, json, os, time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

# The DER key stores the 32-byte seed in the last 32 bytes
der_bytes = base64.b64decode(os.environ['WR_API_SECRET'])
signing_key = Ed25519PrivateKey.from_private_bytes(der_bytes[-32:])

def sign_request(method: str, path: str, body: dict = {}) -> dict:
timestamp = int(time.time())
message = '|'.join([
method.upper(),
path,
str(timestamp),
json.dumps(body, separators=(',', ':')),
])
sig = signing_key.sign(message.encode('utf-8'))
return {'timestamp': timestamp, 'signature': base64.b64encode(sig).decode()}

The server rejects requests where X-Sdk-Timestamp is more than 30 seconds old or set in the future. Keep your system clock synced with NTP.