Skip to content

Authentication

All /v2 endpoints require HMAC-SHA256 request signing.

The API key you sign with also carries fine-grained scopes (resource:action); an endpoint returns 403 (code: INSUFFICIENT_SCOPE) if the key lacks the one it needs. The scope catalogue lives in the API reference, on Create an API key, because it is reference material and changes with the product.

Every request must include three headers:

Header Example
X-CowriePay-Key cpk_live_xxxxxxxxxxxxxxxxxxxx
X-CowriePay-Timestamp 1716825600 (Unix timestamp, seconds)
X-CowriePay-Signature a1b2c3d4... (64-char hex)

The signature is the HMAC-SHA256 of a single string whose parts are joined by literal dots (.):

signing_string = TIMESTAMP + "." + METHOD + "." + PATH + "." + BODY_SHA256
signature = hex( HMAC_SHA256(secret, signing_string) )

Where:

  • TIMESTAMP, the same Unix timestamp (seconds) sent in X-CowriePay-Timestamp
  • METHOD, uppercase HTTP method: GET, POST, PATCH, DELETE
  • PATH, the full request target exactly as sent, including the /v2 prefix and the query string. e.g. /v2/wallets?page=2&status=ACTIVE
  • BODY_SHA256, lowercase SHA-256 hex of the raw request body. For a request with no body (GET, DELETE), use the SHA-256 of the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

The signature is lowercase hex, sent as-is in X-CowriePay-Signature (no prefix). Because the body is hashed, JSON key order and whitespace don’t matter, just hash the exact bytes you send.

const crypto = require('crypto');
function sign(method, path, body, apiKeyId, apiSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash('sha256').update(body ?? '').digest('hex');
const signingString = [timestamp, method.toUpperCase(), path, bodyHash].join('.');
const signature = crypto.createHmac('sha256', apiSecret).update(signingString).digest('hex');
return {
'X-CowriePay-Key': apiKeyId,
'X-CowriePay-Timestamp': timestamp,
'X-CowriePay-Signature': signature,
};
}
// POST with a body, sign and send the SAME string.
const body = JSON.stringify({ chain: 'TRON', asset: 'USDT_TRON' });
const headers = sign('POST', '/v2/wallets', body, 'cpk_live_xxxxxxxxxxxxxxxxxxxx', 'your_secret');
// GET / DELETE, no body, pass '' (and include the query string in the path):
// sign('GET', '/v2/wallets?page=1&limit=20', '', 'cpk_live_xxx', 'your_secret');

Every error response is { "error": "<message>", "code": "<MACHINE_CODE>" }, branch on code, never on the message text. Auth-specific codes:

code HTTP Cause Fix
MISSING_AUTH_HEADERS 401 A required X-CowriePay-* header is absent Send all three headers
TIMESTAMP_EXPIRED 401 Clock skew > 5 minutes Sync your server clock (NTP)
INVALID_SIGNATURE 401 Wrong secret or signing-string construction Log the exact signing string; confirm you hashed the raw body and included /v2 + the query string in PATH
REPLAY_DETECTED 401 The same signature was sent twice within 5 min Use a fresh timestamp on each request
INVALID_API_KEY 401 Unknown or revoked key Check the key id and that it isn’t revoked
INSUFFICIENT_SCOPE 403 The key lacks the required scope Mint a key with the needed scope