Important Note
⚠️ Important: All Requests Must Be Signed
To ensure request authenticity and integrity, all requests sent from your backend to COUNT’s API must be signed using your client secret. This signature helps prevent tampering, replay attacks, and unauthorized access.
All API requests (except Initiate Authorization and token exchange) require two layers of authentication:
-
Partner Authentication (HMAC) — Every request must include an HMAC-SHA256 signature in the headers to verify your application’s identity.
-
User Authentication (Bearer Token) — Data endpoints (chart of accounts, vendors, bills, etc.) also require a Bearer access token to identify which user and workspace you’re acting on behalf of.
Rate Limiting: API requests are rate-limited to 100 requests per minute per client ID. Exceeding this will result in a 429 error.
Redirect URI: Your registered redirect URI must not contain query parameters. Use the state parameter to pass custom context through the OAuth flow — it is returned to you untouched with the authorization code.
IDs: All IDs returned by the API are UUIDs. When referencing resources (workspaces, customers, accounts, etc.), always use the UUID format.
We strongly recommend using a reusable Axios instance with request signing built in, as shown below.
🔐 Signature Requirements
Each request must include three critical headers:
-
x-client-idshould contain your unique client identifier, which you’ll find in your COUNT dashboard. -
x-timestampmust be the current UNIX timestamp in seconds when the request is made. -
x-signatureis the computed HMAC SHA-256 signature based on the request details and your client secret.
The signature is generated using a combination of the HTTP method, the request path (excluding domain), the timestamp, and the hashed request body (for POST, PUT, or PATCH methods). These are combined into a single string, hashed using your client secret, and attached to the request.
📋 Signature Canonicalization Rules
Path Component
Only the URL pathname is used in the signature - query parameters are excluded.
-
✅ Include: The pathname portion of the URL (e.g.,
/customers,/customers/123,/invoices) -
❌ Exclude: Query string parameters (e.g.,
?page=1&limit=10&search=test) -
❌ Exclude: Domain/hostname (e.g.,
https://api.getcount.com) -
❌ Exclude: Protocol (e.g.,
https://)
Examples:
-
URL:
https://api.getcount.com/customers?page=1&limit=10 -
Path used in signature:
/customers(query string ignored) -
URL:
https://api.getcount.com/customers/abc-123?include=balance -
Path used in signature:
/customers/abc-123(query string ignored)
Request Body Hashing
For POST, PUT, and PATCH requests only:
-
The request body must be a JSON object
-
The body is hashed using SHA-256 after JSON stringification
-
Important: JSON is stringified as-is with no canonicalization (key ordering, whitespace, etc. are preserved as you send them)
-
Empty bodies or non-JSON bodies result in an empty string hash
Body Hash Rules:
-
✅ Include body hash for:
POST,PUT,PATCHmethods -
❌ Exclude body hash for:
GET,DELETE,HEAD,OPTIONSmethods (empty string used) -
✅ Hash format: SHA-256 of
JSON.stringify(body) -
⚠️ Note: The exact JSON stringification matters - ensure consistent serialization
HMAC Base String Format
The base string follows this exact format:
Where:
-
METHODis the uppercase HTTP method (e.g.,GET,POST,PUT,PATCH,DELETE) -
/pathis the URL pathname only (no query string, no domain) -
timestampis the UNIX timestamp in seconds (as a string) -
bodyHashis the SHA-256 hash of the JSON body (empty string for GET/DELETE/etc.)
Examples:
-
GET request:
-
Method:
GET -
Path:
/customers -
Timestamp:
1704067200 -
Body Hash: `` (empty for GET)
-
Base String:
GET:/customers:1704067200:
-
-
GET with query parameters:
-
Method:
GET -
Path:
/customers(query string?page=1&limit=10is ignored) -
Timestamp:
1704067200 -
Body Hash: `` (empty for GET)
-
Base String:
GET:/customers:1704067200:
-
-
POST request:
-
Method:
POST -
Path:
/customers -
Timestamp:
1704067200 -
Body:
{"name": "Test Customer", "email": "test@example.com"} -
Body Hash:
a1b2c3d4e5f6...(SHA-256 of JSON string) -
Base String:
POST:/customers:1704067200:a1b2c3d4e5f6...
-
⚠️ Important Notes
-
Query Parameters: Query string parameters (e.g.,
?page=1&limit=10) are NOT included in the signature. Only the pathname is used. -
JSON Body Serialization: The exact JSON stringification matters. Use consistent serialization - the same object must produce the same JSON string every time. Consider:
-
Key ordering (if your JSON library preserves it)
-
Whitespace (if your JSON library includes it)
-
Number formatting
-
Date serialization
-
-
Timestamp Accuracy: Ensure your server time is accurate. Requests with timestamps that are too old or too far in the future will be rejected.
Below is the recommended setup using a reusable Axios client to handle signing automatically.
const axios = require("axios");
const crypto = require("crypto");// Create an Axios instance with base URL and static x-client-id header
const countClient = axios.create({
baseURL: process.env.COUNT_PARTNER_API_ENDPOINT,
headers: {
"x-client-id": process.env.COUNT_CLIENT_ID,
},
});// Attach the interceptor with a reusable signing function
countClient.interceptors.request.use(signRequest, (error) => Promise.reject(error));// Function that handles signing the request
function signRequest(config) {
const method = (config.method || "GET").toUpperCase();
const url = config.url || "/";
const urlPath = new URL(url, config.baseURL).pathname;
const timestamp = Math.floor(Date.now() / 1000).toString();
const clientSecret = process.env.COUNT_CLIENT_SECRET;
const body = config.data || {}; const signature = generateSignature({
method,
path: urlPath,
timestamp,
body,
clientSecret,
}); config.headers["x-timestamp"] = timestamp;
config.headers["x-signature"] = signature; return config;
}// Hash the request body using SHA-256
function hashBody(body) {
return body && Object.keys(body).length > 0
? crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex")
: "";
}// Build the HMAC base string in the format: METHOD:/path:timestamp:bodyHash
function buildHmacBaseString(method, path, timestamp, bodyHash = "") {
return ${method}:${path}:${timestamp}:${bodyHash};
}// Generate the HMAC-SHA256 signature
function generateSignature({ method, path, timestamp, body, clientSecret }) {
const bodyHash = ["POST", "PUT", "PATCH"].includes(method)
? hashBody(body)
: ""; const baseString = buildHmacBaseString(method, path, timestamp, bodyHash); return crypto
.createHmac("sha256", clientSecret)
.update(baseString)
.digest("hex");
}const axios = require("axios");
const crypto = require("crypto");// Create an Axios instance with base URL and static x-client-id header
const countClient = axios.create({
baseURL: process.env.COUNT_PARTNER_API_ENDPOINT,
headers: {
"x-client-id": process.env.COUNT_CLIENT_ID,
},
});// Attach the interceptor with a reusable signing function
countClient.interceptors.request.use(signRequest, (error) => Promise.reject(error));// Function that handles signing the request
function signRequest(config) {
const method = (config.method || "GET").toUpperCase();
const url = config.url || "/";
const urlPath = new URL(url, config.baseURL).pathname;
const timestamp = Math.floor(Date.now() / 1000).toString();
const clientSecret = process.env.COUNT_CLIENT_SECRET;
const body = config.data || {};const signature = generateSignature({
method,
path: urlPath,
timestamp,
body,
clientSecret,
});config.headers["x-timestamp"] = timestamp;
config.headers["x-signature"] = signature;return config;
}// Hash the request body using SHA-256
function hashBody(body) {
return body && Object.keys(body).length > 0
? crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex")
: "";
}// Build the HMAC base string in the format: METHOD:/path:timestamp:bodyHash
function buildHmacBaseString(method, path, timestamp, bodyHash = "") {
return ${method}:${path}:${timestamp}:${bodyHash};
}// Generate the HMAC-SHA256 signature
function generateSignature({ method, path, timestamp, body, clientSecret }) {
const bodyHash = ["POST", "PUT", "PATCH"].includes(method)
? hashBody(body)
: "";const baseString = buildHmacBaseString(method, path, timestamp, bodyHash);return crypto
.createHmac("sha256", clientSecret)
.update(baseString)
.digest("hex");
}Ensure your server time is accurate and your clientSecret is never exposed on the client side. Requests with invalid or expired signatures will be rejected by COUNT’s API.
On this page
- Important Note