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.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.
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.
| 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:- Sort parameter keys alphabetically (lexicographic order)
- For duplicate keys, sort their values alphabetically
- URL-encode keys and values
- Join with
&(no leading?)
?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
Example A: A Filtered GET Request
Example A: A Filtered GET Request
Target URI:
Resulting Canonical String:
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) |
Example B: A Simple GET Request (No Parameters)
Example B: A Simple GET Request (No Parameters)
Target URI:
Resulting Canonical String:(Note the
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) |
|| 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):
- 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.
- 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
\uescapes for non-ASCII or just standard UTF-8 bytes) so the hash does not change based on the computer’s language settings.
- Keys sorted alphabetically (recursive for nested objects): Original:
- Compute SHA-256 hash of the UTF-8 encoded result
{ "z": 1, "a": 2 } becomes {"a":2,"z":1}
For empty bodies (GET, DELETE, or POST with no body), hash an empty byte array:
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
- TypeScript
- Python
- Go
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
- Timestamp precision
- Must be milliseconds, not seconds
- Example:
1706918400000(13 digits), not1706918400(10 digits)
- Nonce format
- Exactly 32 lowercase hex characters (representing 16 random bytes)
- Use cryptographic random:
crypto.randomBytes(),secrets.token_hex(), orcrypto/rand - Example:
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
- 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
- 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
- Empty body handling
- Hash an empty byte array, not
nullorundefined - SHA-256 of empty:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
- Hash an empty byte array, not
- Pathname normalization
- No trailing slashes (except root
/) - Collapse multiple slashes:
//api//v1→/api/v1 - No query string (that’s a separate component)
- No trailing slashes (except root
- Encoding
- UTF-8 for all strings
- Lowercase hex for SHA-256 hash and signature