> ## Documentation Index
> Fetch the complete documentation index at: https://otoyinc.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authenticate API Requests

> All requests to the Dispersed API require authentication using an API Key with HMAC signature verification. 

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:

| Header        | Description                                   | Example                            |
| ------------- | --------------------------------------------- | ---------------------------------- |
| `X-API-Key`   | Your API Key public key                       | `pk_abc123...`                     |
| `X-Time`      | Unix timestamp in **milliseconds**            | `1706918400000`                    |
| `X-Nonce`     | Random 16-byte hex string (32 characters)     | `a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6` |
| `X-Signature` | HMAC-SHA256 signature of the canonical string | `e3b0c442...`                      |

***

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

```text theme={null}
publicKey|timestamp|nonce|METHOD|pathname|queryString|bodySha256
```

**Example:**

```text theme={null}
pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs|limit=10&page=1|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

| Component     | Description                                                             |
| ------------- | ----------------------------------------------------------------------- |
| `publicKey`   | Your API key's public key (`pk_...`) is your unique API identifier      |
| `timestamp`   | Unix timestamp in **milliseconds** (same as `X-Time` header)            |
| `nonce`       | Random 16-byte hex string, 32 characters (same as `X-Nonce` header)     |
| `METHOD`      | HTTP method in all uppercase (`GET`, `POST`, `PATCH`, `DELETE`)         |
| `pathname`    | The URI path starting with `/` (e.g., `/v1/jobs`)                       |
| `queryString` | The **Canonical Query String** (see rules below)                        |
| `bodySha256`  | SHA-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

<AccordionGroup>
  <Accordion title="Example A: A Filtered GET Request">
    **Target URI:** `https://example.com`

    | **Component**   | **Value**                                                                                       |
    | :-------------- | :---------------------------------------------------------------------------------------------- |
    | **publicKey**   | `pk_abc123`                                                                                     |
    | **timestamp**   | `1706918400000`                                                                                 |
    | **nonce**       | `a1b2c3d4e5f6a7b8`                                                                              |
    | **METHOD**      | `GET`                                                                                           |
    | **pathname**    | `/v1/jobs`                                                                                      |
    | **queryString** | `limit=10&page=1`                                                                               |
    | **bodySha256**  | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855...` (Hash of an empty string) |

    **Resulting Canonical String:**

    ```text theme={null}
    pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs|limit=10&page=1|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    ```
  </Accordion>

  <Accordion title="Example B: A Simple GET Request (No Parameters)">
    **Target URI:** `https://example.com`

    | **Component**   | **Value**                                                                                       |
    | :-------------- | :---------------------------------------------------------------------------------------------- |
    | **publicKey**   | `pk_abc123`                                                                                     |
    | **timestamp**   | `1706918400000`                                                                                 |
    | **nonce**       | `a1b2c3d4e5f6a7b8`                                                                              |
    | **METHOD**      | `GET`                                                                                           |
    | **pathname**    | `/v1/jobs`                                                                                      |
    | **queryString** |                                                                                                 |
    | **bodySha256**  | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855...` (Hash of an empty string) |

    **Resulting Canonical String:**

    ```text theme={null}
    pk_abc123|1706918400000|a1b2c3d4e5f6a7b8|GET|/v1/jobs||e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    ```

    *(Note the* `|| `*where the query string would normally be.)*
  </Accordion>
</AccordionGroup>

### 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:

```text theme={null}
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

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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();
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import hmac
    import json
    import os
    import secrets
    import time
    from typing import Any, Optional
    from urllib.parse import urlencode, parse_qs
    import requests

    PUBLIC_KEY = os.environ.get("DISPERSED_PUBLIC_KEY", "pk_your_public_key")
    SECRET_KEY = os.environ.get("DISPERSED_SECRET_KEY", "sk_your_secret_key")
    BASE_URL = os.environ.get("DISPERSED_API_BASE_URL", "https://api.dispersed.com")


    def canonicalize_json(value: Any) -> Any:
        if isinstance(value, dict):
            return {k: canonicalize_json(v) for k, v in sorted(value.items())}
        if isinstance(value, list):
            return [canonicalize_json(item) for item in value]
        return value


    def generate_auth_headers(
        method: str,
        pathname: str,
        query: Optional[dict[str, str]] = None,
        body: Optional[dict[str, Any]] = None,
    ) -> dict[str, str]:
        timestamp = str(int(time.time() * 1000))
        nonce = secrets.token_hex(16)

        query_string = urlencode(sorted((query or {}).items()))

        if body is not None:
            canonical_body = json.dumps(
                canonicalize_json(body),
                separators=(",", ":"),
                ensure_ascii=True,
                allow_nan=False,
            )
            body_sha256 = hashlib.sha256(canonical_body.encode("utf-8")).hexdigest()
        else:
            body_sha256 = hashlib.sha256(b"").hexdigest()

        canonical_string = "|".join([
            PUBLIC_KEY,
            timestamp,
            nonce,
            method.upper(),
            pathname,
            query_string,
            body_sha256,
        ])

        signature = hmac.new(
            key=SECRET_KEY.encode("utf-8"),
            msg=canonical_string.encode("utf-8"),
            digestmod=hashlib.sha256,
        ).hexdigest()

        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
    def list_jobs(page: int = 1, limit: int = 20) -> dict[str, Any]:
        pathname = "/v1/jobs"
        query = {"page": str(page), "limit": str(limit)}
        headers = generate_auth_headers("GET", pathname, query=query)

        response = requests.get(
            f"{BASE_URL}{pathname}",
            params=query,
            headers=headers,
            timeout=30,
        )
        response.raise_for_status()
        return response.json()
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    package main

    import (
    	"bytes"
    	"crypto/hmac"
    	"crypto/rand"
    	"crypto/sha256"
    	"encoding/hex"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"net/url"
    	"os"
    	"sort"
    	"strconv"
    	"strings"
    	"time"
    )

    var (
    	PublicKey = getEnv("DISPERSED_PUBLIC_KEY", "pk_your_public_key")
    	SecretKey = getEnv("DISPERSED_SECRET_KEY", "sk_your_secret_key")
    	BaseURL   = getEnv("DISPERSED_API_BASE_URL", "https://api.dispersed.com")
    )

    func getEnv(key, fallback string) string {
    	if value := os.Getenv(key); value != "" {
    		return value
    	}
    	return fallback
    }

    func canonicalizeQueryString(query url.Values) string {
    	if len(query) == 0 {
    		return ""
    	}
    	keys := make([]string, 0, len(query))
    	for k := range query {
    		keys = append(keys, k)
    	}
    	sort.Strings(keys)

    	var parts []string
    	for _, k := range keys {
    		values := query[k]
    		sortedValues := make([]string, len(values))
    		copy(sortedValues, values)
    		sort.Strings(sortedValues)
    		for _, v := range sortedValues {
    			parts = append(parts, url.QueryEscape(k)+"="+url.QueryEscape(v))
    		}
    	}
    	return strings.Join(parts, "&")
    }

    func generateAuthHeaders(method, pathname string, query url.Values, body interface{}) (http.Header, error) {
    	timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)

    	b := make([]byte, 16)
    	if _, err := rand.Read(b); err != nil {
    		return nil, err
    	}
    	nonce := hex.EncodeToString(b)

    	queryString := canonicalizeQueryString(query)

    	var bodySha256 string
    	if body != nil {
    		jsonBytes, err := json.Marshal(body)
    		if err != nil {
    			return nil, err
    		}
    		hash := sha256.Sum256(jsonBytes)
    		bodySha256 = hex.EncodeToString(hash[:])
    	} else {
    		hash := sha256.Sum256([]byte{})
    		bodySha256 = hex.EncodeToString(hash[:])
    	}

    	canonicalString := strings.Join([]string{
    		PublicKey, timestamp, nonce, strings.ToUpper(method),
    		pathname, queryString, bodySha256,
    	}, "|")

    	mac := hmac.New(sha256.New, []byte(SecretKey))
    	mac.Write([]byte(canonicalString))
    	signature := hex.EncodeToString(mac.Sum(nil))

    	headers := http.Header{}
    	headers.Set("X-API-Key", PublicKey)
    	headers.Set("X-Time", timestamp)
    	headers.Set("X-Nonce", nonce)
    	headers.Set("X-Signature", signature)
    	headers.Set("Content-Type", "application/json")
    	return headers, nil
    }

    // Example: List jobs
    func ListJobs(page, limit int) ([]byte, error) {
    	pathname := "/v1/jobs"
    	query := url.Values{
    		"page":  {strconv.Itoa(page)},
    		"limit": {strconv.Itoa(limit)},
    	}

    	headers, err := generateAuthHeaders("GET", pathname, query, nil)
    	if err != nil {
    		return nil, err
    	}

    	req, _ := http.NewRequest("GET", BaseURL+pathname+"?"+query.Encode(), nil)
    	req.Header = headers

    	resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
    	if err != nil {
    		return nil, err
    	}
    	defer resp.Body.Close()

    	return io.ReadAll(resp.Body)
    }
    ```
  </Tab>
</Tabs>

***

## Common Errors

| Status | Error                    | Cause                                                           | Solution                             |
| ------ | ------------------------ | --------------------------------------------------------------- | ------------------------------------ |
| 400    | Missing required header  | `X-API-Key`, `X-Time`, `X-Nonce`, or `X-Signature` not provided | Include all four headers             |
| 400    | Invalid X-Time header    | `X-Time` is not a valid integer timestamp                       | Use milliseconds since Unix epoch    |
| 400    | Invalid X-Nonce header   | Nonce is not a 32-character hex string                          | Generate 16 random bytes as hex      |
| 400    | Invalid or reused nonce  | Nonce was already used within 24 hours                          | Generate a fresh nonce per request   |
| 401    | Invalid signature        | Signature doesn't match expected value                          | Verify canonical string construction |
| 401    | Invalid API key          | Public key doesn't exist or is revoked/deleted                  | Check your public key is correct     |
| 401    | API key has expired      | Key past expiration date                                        | Create a new API key                 |
| 403    | Timestamp out of range   | `X-Time` differs from server time by more than 5 minutes        | Sync your clock, use current time    |
| 403    | Insufficient permissions | Key lacks required permission for this endpoint                 | Create key with needed permissions   |
| 403    | IP address not allowed   | Request from non-allowlisted IP                                 | Update 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=1` → `a=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:

```text theme={null}
publicKey|timestamp|nonce|METHOD|pathname|queryString|bodySha256
```

Ensure there are exactly **7 components** separated by **6 pipe characters**.

<Warning>
  **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)
  * <u>Never</u> 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
</Warning>
