HMAC Authentication
Coinepay OpenAPI uses HMAC-SHA256 to sign every request. Clients send 4 headers; the server verifies identity + time window + replay protection + payload integrity.
Required Headers
| Header | Description | Example |
|---|---|---|
X-App-Id | Credential identifier, fixed 31 chars (cp_ + 28 hex) | cp_a1b2c3d4e5f6071829304a5b6c7d8e9f |
X-Timestamp | Unix seconds (not ms), ASCII decimal | 1714377600 |
X-Nonce | 8–64 chars, unique per AppID within 10 minutes | 8f7e6d5c4b3a29180a1b2c3d4e5f6071 |
X-Signature | HMAC-SHA256 lowercase hex, 64 chars | 9b8e7f6d5c4b3a... |
Time unit
X-Timestamp must be seconds, not milliseconds. Math.floor(Date.now() / 1000), not Date.now().
Signing Input Construction
signInput = METHOD + LF + PATH + LF + RAW_QUERY + LF + TIMESTAMP + LF + NONCE + LF + BODY_SHA256_HEX| Symbol | Meaning |
|---|---|
LF | Single byte 0x0A (\n), not \r\n |
+ | String concatenation |
Field Definitions
| Field | Value | Notes |
|---|---|---|
METHOD | Uppercase HTTP method (always POST for OpenAPI) | ASCII |
PATH | Request path with leading /, excluding query string | Do not URL-decode/encode |
RAW_QUERY | Query string without ? (empty string if none) | Usually empty |
TIMESTAMP | Same string as X-Timestamp header | ASCII |
NONCE | Same as X-Nonce header | ASCII |
BODY_SHA256_HEX | Lowercase hex of sha256(body bytes) | 64 chars |
SHA256 of empty body
Constant: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. SHA256 of {} is 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a (note the difference).
Computing the Signature
signature = lowercase_hex( HMAC_SHA256( secret_bytes, signInput_bytes ) )Secret encoding
secret_bytes is the UTF-8 bytes of the secret string (i.e. ASCII bytes of the 64 hex chars). Do NOT hex-decode the secret into 32 bytes before HMAC.
Signature length is fixed at 64 hex chars.
Complete Request Example
POST /api/v1/openapi/card_headers/list HTTP/1.1
Host: api.coinepay.net
X-App-Id: cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-Timestamp: 1714377600
X-Nonce: 8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-Signature: 9b8e7f6d5c4b3a2918273645d4e3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1b
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Content-Length: 24
{"page":1,"page_size":20}Replay Protection
| Check | Value |
|---|---|
| Timestamp tolerance | [server_now - 300s, server_now + 300s] (±5 min) |
| Nonce uniqueness window | Same (app_id, nonce) may NOT repeat within 10 minutes |
| Recommended nonce | crypto/rand 16 bytes → 32 hex chars |
Edge Cases
| Scenario | Handling |
|---|---|
body is empty object {} | BODY_SHA256_HEX = 44136fa3...8a |
| body is empty string | Use empty body constant e3b0c44...855 |
| body contains non-ASCII | sha256 over UTF-8 bytes |
body is array [1,2,3] | Hash the literal bytes (key order/whitespace doesn't matter once the bytes are fixed) |
| Network middleware rewrites body | Signature fails; avoid any body-rewriting layer |
Implementation Notes
Stringify must be deterministic
Different languages may serialize JSON differently (key order, whitespace, escapes). Whatever bytes the client stringified are the bytes you must hash AND send. Don't hash one serialization and send another.
Handling 401
The server returns the same openapi_invalid_credentials for all auth-failure causes (unknown AppID / bad signature / clock drift / disabled credential / inactive user). This is anti-enumeration; clients shouldn't try to distinguish causes.
Debugging steps:
- Confirm server time vs client time within 5 min (
date +%son both sides). - Print signInput bytes; confirm
\nis0x0Aand there are no\r. - Confirm
X-Timestampheader equals theTIMESTAMPin signInput exactly (don't compute twice). - Confirm
BODY_SHA256_HEXis over the actual sent bytes, not a re-stringified version. - Reset credentials and retry.
Next
- Idempotency — safe retries on write endpoints
- Code Samples — copy-paste-ready clients