Authenticating Webhook Requests
Verify every webhook before you trust the payload.
Verification Flow
4 stages · SDK availableThree signed headers plus the raw request body — these are the inputs you'll verify before trusting anything inside.
- X-CoinPayments-Client4f8b9d2e-1234-5678-9abc-def012345678
The clientId of the integration that owns this webhook.
- X-CoinPayments-Timestamp2025-03-01T02:30:00
UTC ISO-8601, no timezone suffix.
- X-CoinPayments-Signatureq2DXcW7c0fOvN6m+ZGgFf/c=
Base64 HMAC-SHA256 to verify against.
- Raw body{ "type": "invoiceCompleted", … }
Capture as bytes — before any JSON parsing. A parsed-and-re-serialised body has different whitespace and silently breaks the signature.
Concatenate these six inputs in order, no separators. Four are derived from the request itself; two come from your endpoint configuration.
- 1BOM\ufeffconstant
UTF-8 byte-order mark, always first.
- 2MethodPOSTfrom endpoint
The HTTP method your endpoint received.
- 3URLhttps://your.app/webhooks/cpsfrom endpoint
The exact URL you registered — not what your framework reconstructs.
- 4Client IDvalue of X-CoinPayments-Clientfrom request
Verbatim from the header.
- 5Timestampvalue of X-CoinPayments-Timestampfrom request
Verbatim from the header.
- 6Raw bodybytes captured at stage 1from request
Do not re-encode, re-order keys, or re-format.
The concatenated string from Stage 2.
Your integration's secret — never transmitted with the request.
SHA-256 specifically — not SHA-1, not raw SHA.
The Base64 output from Stage 3.
The header value, verbatim.
- Match
The payload is authentic. Process the event and key off its type / status.
- Mismatch
Drop the request. Do not parse, do not log payload contents, do not retry — treat it as untrusted input.
