Skip to content
Reeflow
Start Building

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.

HMAC signature authentication follows a three-step process:

  1. 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.
  2. Sign each request: For every API call, generate an HMAC‑SHA256 signature over a canonical string that represents the exact request you’ll send.
  3. 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.

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)

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 ID
  • X-API-Timestamp: current Unix time in seconds
  • X-API-Signature: HMAC-SHA256 hex signature
  • Content-Type: when request has a body

Keep these requirements in mind as you implement authentication:

  • Choose one auth method: Send either Authorization (user auth) or X-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

The signature process involves building a canonical string that represents your exact request, then signing it with HMAC-SHA256.

Create a string with five lines (joined by newlines) that captures your request:

  • METHOD (uppercase, like POST or GET)
  • 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
/connections
1730930400
application/json
{"name":"Test Connection","type":"pg","config":{"host":"localhost","port":5432,"database":"testdb","user":"testuser","password":"secret123","ssl":false}}

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:

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

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

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

This 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)

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();
}

Now let’s walk through the complete process of making an authenticated API request. For every request, you’ll follow these three steps:

Create a timestamp, build the canonical string, and generate the HMAC signature:

import { buildCanonicalString, sign } from './hmac-client';
// Request details
const 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 signature
const 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);

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,
};

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);

Running into authentication errors? This table helps you quickly identify and fix the most common issues:

Error (status)Likely causeHow 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.

Most signature errors occur due to canonical string mismatches:

Method case sensitivity

// ❌ Wrong - method must be uppercase
const canonical = ['post', '/connections', timestamp, contentType, body].join('\n');
// ✅ Correct
const canonical = ['POST', '/connections', timestamp, contentType, body].join('\n');

Query parameters

// ❌ Wrong - missing query parameters
const canonical = ['GET', '/connections', timestamp, '', ''].join('\n');
// ✅ Correct - include query string
const canonical = ['GET', '/connections?limit=10', timestamp, '', ''].join('\n');

Content-Type matching

// ❌ Wrong - mismatch between canonical and actual header
const canonical = ['POST', '/connections', timestamp, 'application/json', body].join('\n');
// But actual request has Content-Type: 'text/plain'
// ✅ Correct - exact match
const canonical = ['POST', '/connections', timestamp, 'application/json', body].join('\n');
const headers = { 'Content-Type': 'application/json' };

Body consistency

// ❌ Wrong - different JSON serialization
const canonical = ['POST', '/connections', timestamp, contentType, JSON.stringify(body)].join('\n');
const requestBody = JSON.stringify(body, null, 2); // Different serialization!
// ✅ Correct - same serialization
const requestBody = JSON.stringify(body);
const canonical = ['POST', '/connections', timestamp, contentType, requestBody].join('\n');
  • Headers: X-API-Key, X-API-Timestamp, X-API-Signature, and Content-Type when 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.
  • 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.