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) |
Building the signature
Section titled “Building the signature”The signature is the HMAC-SHA256 of a single string whose parts are joined by literal dots (.):
signing_string = TIMESTAMP + "." + METHOD + "." + PATH + "." + BODY_SHA256signature = hex( HMAC_SHA256(secret, signing_string) )Where:
TIMESTAMP, the same Unix timestamp (seconds) sent inX-CowriePay-TimestampMETHOD, uppercase HTTP method:GET,POST,PATCH,DELETEPATH, the full request target exactly as sent, including the/v2prefix and the query string. e.g./v2/wallets?page=2&status=ACTIVEBODY_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.
Code examples
Section titled “Code examples”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');import hashlib, hmac, time
def sign(method, path, body, api_key_id, api_secret): timestamp = str(int(time.time())) body_hash = hashlib.sha256((body or '').encode()).hexdigest() signing_string = '.'.join([timestamp, method.upper(), path, body_hash]) signature = hmac.new( api_secret.encode(), signing_string.encode(), hashlib.sha256 ).hexdigest() return { 'X-CowriePay-Key': api_key_id, 'X-CowriePay-Timestamp': timestamp, 'X-CowriePay-Signature': signature, }TIMESTAMP=$(date +%s)BODY='{"chain":"TRON","asset":"USDT_TRON"}'BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | sed 's/^.*= //')SIGNING_STRING="$TIMESTAMP.POST./v2/wallets.$BODY_HASH"SIG=$(printf '%s' "$SIGNING_STRING" | openssl dgst -sha256 -hmac "your_secret" | sed 's/^.*= //')
curl -X POST https://api.cowriepay.io/v2/wallets \ -H "Content-Type: application/json" \ -H "X-CowriePay-Key: cpk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-CowriePay-Timestamp: $TIMESTAMP" \ -H "X-CowriePay-Signature: $SIG" \ -d "$BODY"Common errors
Section titled “Common errors”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 |