Skip to main content

Documentation Index

Fetch the complete documentation index at: https://otoyinc.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

To ensure the security and integrity of every request, The Dispersed API uses an HMAC (Hash-based Message Authentication Code) signature. This proves you possess the secret key without ever transmitting it over the network.

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

Authentication: HMAC Signature Scheme

Dispersed API requests require HMAC-SHA256 authentication, constructed by signing a 7-part, pipe-delimited canonical string (publicKey|timestamp|nonce|METHOD|pathname|queryString|bodySha256). Key requirements include sorting query parameters alphabetically (without the leading ”?”), hashing the body with SHA-256 (using an empty string hash for empty bodies), and creating a 64-character lowercase hex signature from the final string. The signature proves you possess the secret key without transmitting it.

Steps to build the HMAC Signature

1. Build the Canonical String

The “Canonical String” is a standardized version of your request. You must concatenate exactly seven components using the pipe (|) character as a 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_...) is your unique API identifier
timestampUnix timestamp in milliseconds (same as X-Time header)
nonceRandom 16-byte hex string, 32 characters (same as X-Nonce header)
METHODHTTP method in all uppercase (GET, POST, PATCH, DELETE)
pathnameThe URI path starting with / (e.g., /v1/jobs)
queryStringThe Canonical Query String (see rules below)
bodySha256SHA-256 hash of the canonicalized request body (hex-encoded, lowercase)

Rules For The Query String (No Leading ?)

The queryString component should contain only the data parameters.
  • DO: limit=10&sort=desc
  • DON’T: ?limit=10&sort=desc (The ? is a separator for the URI, not part of the data).

Handling Empty Fields (||)

If a component is empty (common with queryString or bodySha256), you must still include the pipe delimiter. This maintains the “column” structure of the string.
  • Example of an empty query string: ...|METHOD|pathname||bodySha256
  • The || indicates that the 6th component is an empty 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

Complete Examples

Target URI: https://example.com
ComponentValue
publicKeypk_abc123
timestamp1706918400000
noncea1b2c3d4e5f6a7b8
METHODGET
pathname/v1/jobs
queryStringlimit=10&page=1
bodySha256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855... (Hash of an empty string)
Resulting Canonical String:
pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs|limit=10&page=1|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Target URI: https://example.com
ComponentValue
publicKeypk_abc123
timestamp1706918400000
noncea1b2c3d4e5f6a7b8
METHODGET
pathname/v1/jobs
queryString
bodySha256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855... (Hash of an empty string)
Resulting Canonical String:
pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs||e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
(Note the || where the query string would normally be.)

2. Canonicalize the Request Body

Turn a “messy” JSON object into a single, predictable string so that a hash can be calculated. For JSON bodies (Content-Type: application/json):
  1. Parse the JSON. Before you can secure the data, you must turn it into a “clean” data structure so that your code can manipulate it.
  2. Re-serialize with:
    • Keys sorted alphabetically (recursive for nested objects): Original: {"status": "active", "id": 101} Sorted: {"id": 101, "status": "active"} Recursive: If there is an object inside the object, you must sort those keys too.
    • Compact format (no whitespace): {"key":"value"} not { "key": "value" } Human-readable JSON has spaces and newlines. Computers don’t need them. You must strip all “extra” characters. (Note: No spaces after colons or commas.)
    • ASCII-safe encoding If you have special characters (like emojis or non-English letters), you must ensure they are encoded consistently (usually using \u escapes for non-ASCII or just standard UTF-8 bytes) so the hash does not change based on the computer’s language settings.
  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

3. Sign with HMAC-SHA256

Once you have built the compact, sorted Canonical String, use your Secret Key to sign it using the HMAC-SHA256 algorithm. Your secret key (sk_...) is used to compute the HMAC-SHA256 signature of the canonical string. Output as lowercase hex (64 characters) and pass the resulting hex string in your request headers as X-Signature.

Code Examples

import * as crypto from 'crypto';

// Configuration
const PUBLIC_KEY = process.env.DISNET_PUBLIC_KEY || 'pk_your_public_key';
const SECRET_KEY = process.env.DISNET_SECRET_KEY || 'sk_your_secret_key';
const BASE_URL = process.env.DISNET_API_BASE_URL || 'https://api.dispersed.com';

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