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​
| Practice | Why |
|---|---|
| Store keys in environment variables | Never hardcode ws_ keys in source code |
| Use restricted keys for automation | Set allowedWorkflowIds to limit blast radius if a key is exposed |
| Rotate immediately on exposure | Dashboard → 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);
});
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.
| Plan | Soft limit (req/s) | Hard limit (req/h) |
|---|---|---|
| Starter | 5 | 9,000 |
| Growth | 10 | 18,000 |
| Professional | 25 | 45,000 |
| Enterprise | Custom | Custom |
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​
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:
| Header | Value |
|---|---|
X-Api-Key | Your Workspace API key (ws_...) |
X-Sdk-Timestamp | Unix epoch in seconds |
X-Sdk-Signature | Base64 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.
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.