HMAC Signature Mode
HMAC Signature Mode provides the highest level of security for API authentication. It uses HMAC-SHA256 signatures with timestamp validation to ensure request authenticity and prevent tampering.
How it works
Section titled “How it works”HMAC signature authentication follows a three-step process:
- Get your credentials: Obtain an API key (ID and secret) from the Reeflow Console. The key ID identifies your organization, while the secret signs your requests.
- Sign each request: For every API call, generate an HMAC‑SHA256 signature over a canonical string that represents the exact request you’ll send.
- Include authentication headers: Send the signature and related information in HTTP headers (
X-API-Key,X-API-Timestamp,X-API-Signature) with every request.
When you send a request, our server reconstructs the same canonical string, verifies your timestamp is recent, computes the expected signature using your key’s secret, and compares it using a constant‑time check for security.
Security benefits
Section titled “Security benefits”HMAC Signature Mode provides several security advantages over Basic Authentication:
- No secrets in transit: Your API secret never travels over the network
- Replay attack protection: Timestamp validation prevents old requests from being replayed
- Request integrity: Any modification to the request invalidates the signature
- Tamper detection: Changes to method, path, body, or headers are detected
- Time-bounded validity: Each request is only valid for a short time window (~5 minutes)
What you’ll need
Section titled “What you’ll need”API credentials (from your Reeflow Console):
- API key ID (starts with
key_) - API key secret (64-character random string)
HTTP headers (required on every request):
X-API-Key: your API key IDX-API-Timestamp: current Unix time in secondsX-API-Signature: HMAC-SHA256 hex signatureContent-Type: when request has a body
Important rules
Section titled “Important rules”Keep these requirements in mind as you implement authentication:
- Choose one auth method: Send either
Authorization(user auth) orX-API-Key(API key auth), never both - Fresh timestamps only: Requests older or newer than ~5 minutes are rejected for security
- Sign what you send: Calculate the signature from exactly what goes over the wire
Creating the signature
Section titled “Creating the signature”The signature process involves building a canonical string that represents your exact request, then signing it with HMAC-SHA256.
Canonical string format
Section titled “Canonical string format”Create a string with five lines (joined by newlines) that captures your request:
METHOD(uppercase, likePOSTorGET)PATH?QUERY(example:/connections?limit=10)TIMESTAMP(Unix seconds as a string)CONTENT-TYPE(empty string if no body)BODY(raw request body; empty string for GET requests)
Here’s what a canonical string looks like for a POST request with JSON:
POST/connections1730930400application/json{"name":"Test Connection","type":"pg","config":{"host":"localhost","port":5432,"database":"testdb","user":"testuser","password":"secret123","ssl":false}}Signature generation process
Section titled “Signature generation process”Once you have your canonical string, you generate the signature using the HMAC-SHA256 algorithm with your API key secret. Here’s how the signature generation works:
-
Use your API key secret as the HMAC key: The secret from your API key pair serves as the cryptographic key for the HMAC operation.
-
Apply HMAC-SHA256 to the canonical string: The HMAC (Hash-based Message Authentication Code) algorithm combines your secret key with the canonical string using SHA-256 as the underlying hash function. This creates a unique signature that can only be generated by someone with your secret.
-
Convert to hexadecimal: The resulting binary signature is converted to a hexadecimal string for transmission in the HTTP header.
The mathematical process looks like this:
signature = HMAC-SHA256(secret_key, canonical_string) → hex_stringThis approach ensures that:
- Only holders of the secret key can generate valid signatures
- Any tampering with the request data will result in signature verification failure
- The signature proves both authenticity (you sent it) and integrity (it wasn’t modified)
Signing functions
Section titled “Signing functions”Here’s a reusable helper that handles the canonical string building and signing:
import { createHmac } from 'node:crypto';
/** * Builds a canonical string representation of an HTTP request for signature generation. * The canonical string format ensures consistent signing across different requests. * @param method - The HTTP method (GET, POST, etc.) - will be converted to uppercase * @param pathWithQuery - The request path including query parameters (e.g., '/connections?limit=10') * @param timestamp - Unix timestamp in seconds as a string * @param contentType - The Content-Type header value, or empty string if no body * @param body - The raw request body as a string, or empty string for requests without body * @returns A newline-separated canonical string ready for HMAC signing */export function buildCanonicalString( method: string, pathWithQuery: string, timestamp: string, contentType: string, body: string,): string { return [method.toUpperCase(), pathWithQuery, timestamp, contentType, body].join('\n');}
/** * Generates an HMAC-SHA256 signature for the given canonical string. * This signature is used to authenticate API requests with Reeflow. * @param secret - The API key secret used as the HMAC key * @param canonical - The canonical string representation of the request * @returns The HMAC-SHA256 signature as a hexadecimal string */export function sign(secret: string, canonical: string): string { return createHmac('sha256', secret).update(canonical).digest('hex');}
type RequestParams = { baseUrl: string; // e.g., 'https://api.reeflow.com' pathWithQuery: string; // e.g., '/connections?limit=10' method?: string; // default 'POST' apiKeyId: string; apiKeySecret: string; body?: unknown; // JSON-serializable; omit for GET};
/** * Makes an authenticated API call to Reeflow using HMAC-SHA256 signature authentication. * Handles signature generation, request construction, and error handling automatically. * @param params - The request parameters object * @param params.baseUrl - The base URL of the API (e.g., 'https://api.reeflow.com') * @param params.pathWithQuery - The request path with query parameters (e.g., '/connections?limit=10') * @param params.method - The HTTP method to use, defaults to 'POST' * @param params.apiKeyId - The API key ID for authentication * @param params.apiKeySecret - The API key secret for signing requests * @param params.body - The request body data (will be JSON stringified if present) * @returns Promise resolving to the parsed JSON response * @throws Error if the request fails with details about the failure */export async function callApi({ baseUrl, pathWithQuery, method = 'POST', apiKeyId, apiKeySecret, body,}: RequestParams) { const url = new URL(pathWithQuery, baseUrl).toString(); const ts = Math.floor(Date.now() / 1000).toString(); const hasBody = body !== undefined && body !== null; const contentType = hasBody ? 'application/json' : ''; const rawBody = hasBody ? JSON.stringify(body) : '';
const canonical = buildCanonicalString(method, pathWithQuery, ts, contentType, rawBody); const signature = sign(apiKeySecret, canonical);
const res = await fetch(url, { method, headers: { 'X-API-Key': apiKeyId, 'X-API-Timestamp': ts, 'X-API-Signature': signature, ...(hasBody ? { 'Content-Type': contentType } : {}), }, body: hasBody ? rawBody : undefined, });
if (!res.ok) { throw new Error(`Request failed: ${res.status} ${res.statusText} — ${await res.text()}`); }
return res.json();}import hmacimport hashlib
def build_canonical_string( method: str, path_with_query: str, timestamp: str, content_type: str, body: str,) -> str: """ Builds a canonical string representation of an HTTP request for signature generation. The canonical string format ensures consistent signing across different requests.
Args: method: The HTTP method (GET, POST, etc.) - will be converted to uppercase path_with_query: The request path including query parameters (e.g., '/connections?limit=10') timestamp: Unix timestamp in seconds as a string content_type: The Content-Type header value, or empty string if no body body: The raw request body as a string, or empty string for requests without body
Returns: A newline-separated canonical string ready for HMAC signing """ return "\n".join([ method.upper(), path_with_query, timestamp, content_type, body, ])
def sign(secret: str, canonical: str) -> str: """ Generates an HMAC-SHA256 signature for the given canonical string. This signature is used to authenticate API requests with Reeflow.
Args: secret: The API key secret used as the HMAC key canonical: The canonical string representation of the request
Returns: The HMAC-SHA256 signature as a hexadecimal string """ return hmac.new( secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256, ).hexdigest()Making authenticated requests
Section titled “Making authenticated requests”Now let’s walk through the complete process of making an authenticated API request. For every request, you’ll follow these three steps:
Step 1: Generate the signature
Section titled “Step 1: Generate the signature”Create a timestamp, build the canonical string, and generate the HMAC signature:
import { buildCanonicalString, sign } from './hmac-client';
// Request detailsconst method = 'POST';const pathWithQuery = '/connections';const body = { name: 'Test Connection', type: 'pg', config: { host: 'localhost', port: 5432, database: 'testdb', user: 'testuser', password: 'secret123', ssl: false, },};const contentType = 'application/json';
// Generate timestamp and signatureconst timestamp = Math.floor(Date.now() / 1000).toString();const rawBody = JSON.stringify(body);const canonical = buildCanonicalString(method, pathWithQuery, timestamp, contentType, rawBody);const signature = sign(process.env.API_KEY_SECRET!, canonical);import jsonimport osimport timefrom hmac_client import build_canonical_string, sign
# Request detailsmethod = 'POST'path_with_query = '/connections'body = { 'name': 'Test Connection', 'type': 'pg', 'config': { 'host': 'localhost', 'port': 5432, 'database': 'testdb', 'user': 'testuser', 'password': 'secret123', 'ssl': False }}content_type = 'application/json'
# Generate timestamp and signaturetimestamp = str(int(time.time()))raw_body = json.dumps(body, separators=(',', ':'))canonical = build_canonical_string(method, path_with_query, timestamp, content_type, raw_body)signature = sign(os.environ['API_KEY_SECRET'], canonical)Step 2: Add authentication headers
Section titled “Step 2: Add authentication headers”Include the required headers with your request:
const headers = { 'X-API-Key': process.env.API_KEY_ID!, 'X-API-Timestamp': timestamp, 'X-API-Signature': signature, 'Content-Type': contentType,};headers = { 'X-API-Key': os.environ['API_KEY_ID'], 'X-API-Timestamp': timestamp, 'X-API-Signature': signature, 'Content-Type': content_type,}Step 3: Send the request
Section titled “Step 3: Send the request”Make the HTTP request with your signed headers:
const baseUrl = process.env.REEFLOW_API_BASE || 'https://api.reeflow.com';const url = new URL(pathWithQuery, baseUrl).toString();
const response = await fetch(url, { method, headers, body: rawBody,});
if (!response.ok) { throw new Error(`Request failed: ${response.status} ${response.statusText}`);}
const data = await response.json();console.log(data);import requestsfrom urllib.parse import urljoin
base_url = os.environ.get('REEFLOW_API_BASE', 'https://api.reeflow.com')url = urljoin(base_url, path_with_query)
response = requests.request( method=method, url=url, headers=headers, data=raw_body)
if not response.ok: raise Exception(f'Request failed: {response.status_code} {response.reason}')
data = response.json()print(data)Troubleshooting
Section titled “Troubleshooting”Running into authentication errors? This table helps you quickly identify and fix the most common issues:
| Error (status) | Likely cause | How to fix |
|---|---|---|
| Invalid API signature (401) | Canonical string mismatch: method not uppercased; path missing the query; full URL used instead of path+query; wrong/missing content type; body differs from bytes sent; wrong secret. | Build the canonical string as METHOD\nPATH?QUERY\nTIMESTAMP\nCONTENT-TYPE\nBODY. Uppercase the method, include the query string, use only path+query, match the exact Content-Type or empty string, and sign the exact body you send (same JSON.stringify). Verify the correct secret. |
| Invalid or missing X-API-Timestamp (401) | Header missing, in milliseconds instead of seconds, or clock skew > ~5 minutes. | Set X-API-Timestamp to Math.floor(Date.now()/1000).toString() and ensure system time is synced. |
| Invalid API key (401) | API key ID not found or revoked. | Use a valid API key ID; check environment variables and rotate keys if necessary. |
| API key is disabled (403) | API key has been disabled in the console. | Re-enable the API key in your Reeflow Console. |
| Multiple credentials provided (400) | Both Authorization and X-API-Key headers present. | Send only one authentication method per request. |
Common signature issues
Section titled “Common signature issues”Most signature errors occur due to canonical string mismatches:
Method case sensitivity
// ❌ Wrong - method must be uppercaseconst canonical = ['post', '/connections', timestamp, contentType, body].join('\n');
// ✅ Correctconst canonical = ['POST', '/connections', timestamp, contentType, body].join('\n');Query parameters
// ❌ Wrong - missing query parametersconst canonical = ['GET', '/connections', timestamp, '', ''].join('\n');
// ✅ Correct - include query stringconst canonical = ['GET', '/connections?limit=10', timestamp, '', ''].join('\n');Content-Type matching
// ❌ Wrong - mismatch between canonical and actual headerconst canonical = ['POST', '/connections', timestamp, 'application/json', body].join('\n');// But actual request has Content-Type: 'text/plain'
// ✅ Correct - exact matchconst canonical = ['POST', '/connections', timestamp, 'application/json', body].join('\n');const headers = { 'Content-Type': 'application/json' };Body consistency
// ❌ Wrong - different JSON serializationconst canonical = ['POST', '/connections', timestamp, contentType, JSON.stringify(body)].join('\n');const requestBody = JSON.stringify(body, null, 2); // Different serialization!
// ✅ Correct - same serializationconst requestBody = JSON.stringify(body);const canonical = ['POST', '/connections', timestamp, contentType, requestBody].join('\n');Reference
Section titled “Reference”- Headers:
X-API-Key,X-API-Timestamp,X-API-Signature, andContent-Typewhen a body is present. - Signing algorithm: HMAC-SHA256 with hex output.
- Canonical string fields (newline-separated): METHOD, PATH?QUERY, TIMESTAMP, CONTENT-TYPE, BODY.
- Timestamp tolerance: Requests must be within ~5 minutes of server time.
- Character encoding: UTF-8 for all string operations.
Next steps
Section titled “Next steps”- Getting started? Try Basic Authentication for faster integration
- Need help? Check the API Reference for complete endpoint documentation
- Production ready? You’re using the most secure authentication method available
HMAC Signature Mode provides enterprise-grade security through cryptographic signatures, replay protection, and request integrity verification. While it requires more implementation effort than Basic Authentication, it offers the strongest security guarantees for production applications.