Authentication
API keys, key types, and scopes for the Zafeguard SDK.
Authentication
Zafeguard uses API keys to authenticate requests. The TypeScript SDK injects your key as the X-Api-Key header on every request — you only need to provide it once at client construction.
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 '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!, // ws_...
});WorkflowClient
Use a Workspace API key and a workflow UUID with WorkflowClient to trigger and inspect workflow runs:
import { WorkflowClient } from '@zafeguard/caller-sdk';
const workflow = new WorkflowClient({
apiKey: process.env.ZAFEGUARD_API_KEY!, // ws_...
workflowId: process.env.ZAFEGUARD_WORKFLOW_ID!, // UUID of the workflow
});Get it: Dashboard → Workspace Settings → API Keys → Create
Scoping keys to specific workflows
When you create a key in Dashboard → Workspace Settings → API Keys, the form takes a label and a list of allowed workflows. Leave the list empty to allow all workflows; choose specific workflows to restrict the key.
Requests that target a workflow not in the key's 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, Zafeguard signs {x-signature-timestamp}.{rawBody} with HMAC-SHA256 and sends the result in the x-signature header as sha256=<hex>. The timestamp is an ISO-8601 string in the x-signature-timestamp header.
Node.js
import { createHmac, timingSafeEqual } from 'crypto';
function verifyCallback(
rawBody: Buffer,
headers: Record<string, string | string[] | undefined>,
secret: string,
): boolean {
const timestamp = headers['x-signature-timestamp'] as string;
const signatureHeader = headers['x-signature'] as string;
const signedPayload = `${timestamp}.${rawBody.toString()}`;
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const received = signatureHeader.replace('sha256=', '');
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) => {
if (!verifyCallback(req.body, req.headers, process.env.ZAFEGUARD_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, headers: dict, secret: str) -> bool:
timestamp = headers['x-signature-timestamp']
signed_payload = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()
received = headers['x-signature'].replace('sha256=', '')
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
The TypeScript SDK (@zafeguard/caller-sdk) does not require you to sign requests — it sends X-Api-Key alone. This section is for callers issuing raw HTTP requests against the API.
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 ZAFEGUARD_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.ZAFEGUARD_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.zafeguard.com/v1/sdk/components', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.ZAFEGUARD_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['ZAFEGUARD_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.