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

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, 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.

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

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:

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 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.

On this page