Skip to content

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

HeaderDescriptionExample
X-App-IdCredential identifier, fixed 31 chars (cp_ + 28 hex)cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-TimestampUnix seconds (not ms), ASCII decimal1714377600
X-Nonce8–64 chars, unique per AppID within 10 minutes8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-SignatureHMAC-SHA256 lowercase hex, 64 chars9b8e7f6d5c4b3a...

Time unit

X-Timestamp must be seconds, not milliseconds. Math.floor(Date.now() / 1000), not Date.now().

Signing Input Construction

text
signInput = METHOD + LF + PATH + LF + RAW_QUERY + LF + TIMESTAMP + LF + NONCE + LF + BODY_SHA256_HEX
SymbolMeaning
LFSingle byte 0x0A (\n), not \r\n
+String concatenation

Field Definitions

FieldValueNotes
METHODUppercase HTTP method (always POST for OpenAPI)ASCII
PATHRequest path with leading /, excluding query stringDo not URL-decode/encode
RAW_QUERYQuery string without ? (empty string if none)Usually empty
TIMESTAMPSame string as X-Timestamp headerASCII
NONCESame as X-Nonce headerASCII
BODY_SHA256_HEXLowercase hex of sha256(body bytes)64 chars

SHA256 of empty body

Constant: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. SHA256 of {} is 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a (note the difference).

Computing the Signature

text
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

http
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

CheckValue
Timestamp tolerance[server_now - 300s, server_now + 300s] (±5 min)
Nonce uniqueness windowSame (app_id, nonce) may NOT repeat within 10 minutes
Recommended noncecrypto/rand 16 bytes → 32 hex chars

Edge Cases

ScenarioHandling
body is empty object {}BODY_SHA256_HEX = 44136fa3...8a
body is empty stringUse empty body constant e3b0c44...855
body contains non-ASCIIsha256 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 bodySignature 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:

  1. Confirm server time vs client time within 5 min (date +%s on both sides).
  2. Print signInput bytes; confirm \n is 0x0A and there are no \r.
  3. Confirm X-Timestamp header equals the TIMESTAMP in signInput exactly (don't compute twice).
  4. Confirm BODY_SHA256_HEX is over the actual sent bytes, not a re-stringified version.
  5. Reset credentials and retry.

Next

Released under MIT-equivalent terms.