Skip to main content

Required Headers

Every API request must include these four headers:
HeaderDescriptionExample
X-API-KeyYour API Key public keypk_abc123...
X-TimeUnix timestamp in milliseconds1706918400000
X-NonceRandom 16-byte hex string (32 characters)a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
X-SignatureHMAC-SHA256 signature of the canonical stringe3b0c442...

HMAC Signature Scheme

The signature proves you possess the secret key without transmitting it. Here’s how it works:

1. Build the Canonical String

Concatenate these seven components with | (pipe) as the delimiter:
publicKey|timestamp|nonce|METHOD|pathname|queryString|bodySha256
Example:
pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs|limit=10&page=1|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
ComponentDescription
publicKeyYour API key’s public key (pk_...)
timestampUnix timestamp in milliseconds (same as X-Time header)
nonceRandom 16-byte hex string, 32 characters (same as X-Nonce header)
METHODHTTP method in uppercase (GET, POST, PATCH, DELETE)
pathnameURL path without query string, normalized (e.g., /v1/jobs)
queryStringCanonicalized query string (sorted alphabetically, no ? prefix)
bodySha256SHA-256 hash of the canonicalized request body (hex-encoded, lowercase)

2. Canonicalize the Query String

Sort query parameters to ensure deterministic signatures:
  1. Sort parameter keys alphabetically (lexicographic order)
  2. For duplicate keys, sort their values alphabetically
  3. URL-encode keys and values
  4. Join with & (no leading ?)
Example: ?z=3&a=1&b=2 becomes a=1&b=2&z=3 Multi-value example: ?tag=zebra&tag=apple becomes tag=apple&tag=zebra

3. Canonicalize the Request Body

For JSON bodies (Content-Type: application/json):
  1. Parse the JSON
  2. Re-serialize with:
    • Keys sorted alphabetically (recursive for nested objects)
    • Compact format (no whitespace): {"key":"value"} not { "key": "value" }
    • ASCII-safe encoding
  3. Compute SHA-256 hash of the UTF-8 encoded result
Example: { "z": 1, "a": 2 } becomes {"a":2,"z":1} For empty bodies (GET, DELETE, or POST with no body), hash an empty byte array:
sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

4. Sign with HMAC-SHA256

Use your secret key (sk_...) to compute an HMAC-SHA256 signature of the canonical string. Output as lowercase hex (64 characters).

Code Examples

import * as crypto from 'crypto';

// Configuration
const PUBLIC_KEY = process.env.DISPERSED_PUBLIC_KEY || 'pk_your_public_key';
const SECRET_KEY = process.env.DISPERSED_SECRET_KEY || 'sk_your_secret_key';
const BASE_URL = process.env.DISPERSED_API_BASE_URL || 'https://api.compute.x.io';

function canonicalizeJson(value: unknown): unknown {
  if (value === null || typeof value !== 'object') {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map(canonicalizeJson);
  }
  const sorted: Record<string, unknown> = {};
  for (const key of Object.keys(value as object).sort()) {
    sorted[key] = canonicalizeJson((value as Record<string, unknown>)[key]);
  }
  return sorted;
}

function canonicalizeQueryString(params: URLSearchParams): string {
  const uniqueKeys = [...new Set(Array.from(params.keys()))].sort();
  return uniqueKeys
    .flatMap((key) => {
      const values = params.getAll(key).sort();
      return values.map(
        (value) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
      );
    })
    .join('&');
}

function generateAuthHeaders(
  method: string,
  pathname: string,
  query: URLSearchParams | Record<string, string> = new URLSearchParams(),
  body: unknown = null
): Record<string, string> {
  const timestamp = Date.now().toString();
  const nonce = crypto.randomBytes(16).toString('hex');

  const params =
    query instanceof URLSearchParams ? query : new URLSearchParams(query);
  const queryString = canonicalizeQueryString(params);

  let bodySha256: string;
  if (body !== null && body !== undefined) {
    const canonicalBody = JSON.stringify(canonicalizeJson(body));
    bodySha256 = crypto
      .createHash('sha256')
      .update(canonicalBody)
      .digest('hex');
  } else {
    bodySha256 = crypto.createHash('sha256').update('').digest('hex');
  }

  const canonicalString = [
    PUBLIC_KEY,
    timestamp,
    nonce,
    method.toUpperCase(),
    pathname,
    queryString,
    bodySha256,
  ].join('|');

  const signature = crypto
    .createHmac('sha256', SECRET_KEY)
    .update(canonicalString)
    .digest('hex');

  return {
    'X-API-Key': PUBLIC_KEY,
    'X-Time': timestamp,
    'X-Nonce': nonce,
    'X-Signature': signature,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  };
}

// Example: List jobs
async function listJobs(page = 1, limit = 20): Promise<unknown> {
  const query = new URLSearchParams({
    page: String(page),
    limit: String(limit),
  });
  const pathname = '/v1/jobs';
  const headers = generateAuthHeaders('GET', pathname, query);

  const response = await fetch(`${BASE_URL}${pathname}?${query}`, {
    method: 'GET',
    headers,
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API Error: ${error.error?.message || response.statusText}`);
  }

  return response.json();
}

Common Errors

StatusErrorCauseSolution
400Missing required headerX-API-Key, X-Time, X-Nonce, or X-Signature not providedInclude all four headers
400Invalid X-Time headerX-Time is not a valid integer timestampUse milliseconds since Unix epoch
400Invalid X-Nonce headerNonce is not a 32-character hex stringGenerate 16 random bytes as hex
400Invalid or reused nonceNonce was already used within 24 hoursGenerate a fresh nonce per request
401Invalid signatureSignature doesn’t match expected valueVerify canonical string construction
401Invalid API keyPublic key doesn’t exist or is revoked/deletedCheck your public key is correct
401API key has expiredKey past expiration dateCreate a new API key
403Timestamp out of rangeX-Time differs from server time by more than 5 minutesSync your clock, use current time
403Insufficient permissionsKey lacks required permission for this endpointCreate key with needed permissions
403IP address not allowedRequest from non-allowlisted IPUpdate key’s allowed IPs

Troubleshooting Signatures

If you’re getting “Invalid signature” errors, verify each component of the canonical string:

Checklist

  1. Timestamp precision
    • Must be milliseconds, not seconds
    • Example: 1706918400000 (13 digits), not 1706918400 (10 digits)
  2. Nonce format
    • Exactly 32 lowercase hex characters (representing 16 random bytes)
    • Use cryptographic random: crypto.randomBytes(), secrets.token_hex(), or crypto/rand
    • Example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
  3. Body canonicalization
    • JSON keys sorted recursively (nested objects too)
    • Compact format with no whitespace: {"a":1,"b":2} not { "a": 1, "b": 2 }
    • Use separators=(",", ":") in Python, compact marshaling in Go/TypeScript
  4. Query string canonicalization
    • Sort both keys and values alphabetically
    • No leading ? character
    • Use RFC 3986 percent-encoding (spaces as %20, not +)
    • Example: ?z=3&a=1a=1&z=3
  5. Empty body handling
    • Hash an empty byte array, not null or undefined
    • SHA-256 of empty: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  6. Pathname normalization
    • No trailing slashes (except root /)
    • Collapse multiple slashes: //api//v1/api/v1
    • No query string (that’s a separate component)
  7. Encoding
    • UTF-8 for all strings
    • Lowercase hex for SHA-256 hash and signature

Debug Your Canonical String

Print your canonical string before signing to verify each component:
publicKey|timestamp|nonce|METHOD|pathname|queryString|bodySha256
Ensure there are exactly 7 components separated by 6 pipe characters.
Keep it Secret, Keep it Safe
  • Preferred: Store secret keys in a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault)
  • Alternative: Use environment variables if a secrets manager is not available (NOTE: environment variables can be exposed through process listings, logs, or crash dumps)
  • Never hardcode keys in source code or commit them to version control
  • Use cryptographically secure random number generators for nonces
  • Implement request timeouts to prevent hanging connections
  • Rotate API keys periodically and revoke unused keys promptly