Skip to content
>
Mode: standard

Authenticating Webhook Requests

Webhook Security · HMAC-SHA256

Verify every webhook before you trust the payload.

Webhooks from CoinPayments are signed with the same three headers your outbound API requests carry. Verification is the signing math in reverse: rebuild the canonical string from the headers and the raw body, HMAC it with your client secret, and constant-time compare against the supplied signature.

Verification Flow

4 stages · SDK available
Stage 1Receive· What lands at your endpoint
POSThttps://your.app/webhooks/cps

Three 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.

Stage 2Reconstruct· Rebuild the message we signed

Concatenate these six inputs in order, no separators. Four are derived from the request itself; two come from your endpoint configuration.

  • 1
    BOM\ufeffconstant

    UTF-8 byte-order mark, always first.

  • 2
    MethodPOSTfrom endpoint

    The HTTP method your endpoint received.

  • 3
    URLhttps://your.app/webhooks/cpsfrom endpoint

    The exact URL you registered — not what your framework reconstructs.

  • 4
    Client IDvalue of X-CoinPayments-Clientfrom request

    Verbatim from the header.

  • 5
    Timestampvalue of X-CoinPayments-Timestampfrom request

    Verbatim from the header.

  • 6
    Raw bodybytes captured at stage 1from request

    Do not re-encode, re-order keys, or re-format.

Stage 3Compute· Sign it the same way we did
Input
Message

The concatenated string from Stage 2.

Key
Client secret

Your integration's secret — never transmitted with the request.

Algorithm
HMAC-SHA256

SHA-256 specifically — not SHA-1, not raw SHA.

HMAC-SHA256(message, secret)32-byte digest
Base64(digest)candidate signature string
Stage 4Compare· Byte-for-byte, in constant time
Computed
Your candidate signature

The Base64 output from Stage 3.

Received
X-CoinPayments-Signature

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.

Use a constant-time comparator (timingSafeEqual, hash_equals, hmac.compare_digest, …) — early-exit string compare leaks signature bytes through timing.

Pitfalls

The ways verification goes wrong
Use the raw body, not the parsed body
Capture the request body before any JSON parsing. Re-serialising a parsed object produces different whitespace and your signature will silently mismatch on every request.
Use the URL you registered
Behind proxies and load balancers, frameworks may reconstruct the request URL with a different scheme, host, or trailing slash. Pin the URL string you registered with CoinPayments and use that in the canonical message.
Constant-time comparison only
Never compare the expected and supplied signatures with == or === — use timingSafeEqual, hash_equals, or hmac.compare_digest. Early-exit string compare leaks signature bytes through timing.
Reject stale timestamps
Treat the X-CoinPayments-Timestamp value as UTC ISO-8601 without a zone (e.g. 2025-03-01T02:30:00) and reject anything more than ~5 minutes off your clock — that stops captured-request replays.