Signatures
CyberPay signatures use HMAC-SHA256, with the resulting bytes encoded in standard Base64. The header and the content to sign depend on the request or response. Body signatures cover the serialized JSON body; card v3 signatures cover only the fields listed below.
Use the signature secret key for the brand associated with the transaction. This is separate from your X-API-KEY. Use the key exactly as supplied, as a UTF-8 string; do not decode it as hex or Base64. Contact CyberPay support to obtain the key and keep it on your server.
Signature formats
| Request or response | Header | Content to sign |
|---|---|---|
Body-signed API requests, including /confirm-payment and /settle-payout | X-Signature | JSON.stringify(payload) |
| Responses from body-signed endpoints | X-Signature | JSON.stringify(responseBody) |
| Payment and payout webhooks, including card transactions | X-Signature | JSON.stringify(webhookPayload) |
Card POST /v3/payment requests | Signature | paymentOptionId:merchantReference:currency:amount |
| Signed card v3 business responses | X-Signature | result.transactionId:result.merchantReference:result.currency:result.amount:result.status |
Request-signature enforcement is optional per brand. When enabled, a missing or mismatched signature on an endpoint that checks it returns HTTP 403. Your API key is still required. Webhooks are signed independently of this setting; verify them before acting on their contents.
Currently, POST /v3/payout, non-card POST /v3/payment, and the v3 options endpoints do not validate these signature headers or return signed responses. Use the applicable format for the later /settle-payout or /confirm-payment call and for webhooks. Do not assume every API response, including HTTP error responses, carries a signature.
Signing outgoing requests
The body-signature format applies to /payment-session, /payout-session, /confirm-payment, /cancel-payment, /settle-payout, and /refund/by-reference.
- Prepare the payload for the endpoint.
- Serialize it with
JSON.stringify(payload). - Compute HMAC-SHA256 using your brand's signature key and the UTF-8 bytes of that string.
- Encode the HMAC bytes in Base64 and send the value in
X-Signature.
The formula is:
Base64(HMAC-SHA256(signatureKey, stringToSign))
The gateway serializes the parsed body before checking the signature. Sign the compact JSON serialization, rather than pretty-printed request text. Preserve property order, value types, and array order. Other languages must produce the same string as JavaScript's JSON.stringify, including number formatting and character escaping. Do not sort keys or add a trailing newline.
For /payment-session and /payout-session, empty string object properties are converted to null before signature verification, including properties in nested objects. Prefer omitting optional empty values or sending null where accepted, and sign the normalized payload.
For multipart /confirm-payment, sign the JSON serialization of the text fields in their submitted order. Field values are strings. The uploaded document file, multipart boundaries, and file bytes are excluded. See the confirmation guide.
Node.js helpers
The following helpers sign a string and compare the exact Base64 header value. The comparison uses Node.js timingSafeEqual after checking that the buffers have equal lengths.
const { createHmac, timingSafeEqual } = require('node:crypto');
/** Sign the exact UTF-8 string using the brand's signature key. */
function signString(stringToSign, signatureKey) {
return createHmac('sha256', signatureKey)
.update(stringToSign, 'utf8')
.digest('base64');
}
/** Reject missing or mismatched signatures without a direct string comparison. */
function verifyStringSignature(stringToSign, receivedSignature, signatureKey) {
if (typeof receivedSignature !== 'string') return false;
const expected = Buffer.from(signString(stringToSign, signatureKey), 'utf8');
const received = Buffer.from(receivedSignature, 'utf8');
return received.length === expected.length && timingSafeEqual(received, expected);
}
For example, to sign a /settle-payout request, use the helpers above and load signatureKey and apiKey from your server configuration:
const payload = {
action: 'APPROVE',
transactionId: '4d3a64a8-7c2b-4e57-8f19-df930b69f312',
};
const body = JSON.stringify(payload);
const headers = {
'Content-Type': 'application/json',
'X-API-KEY': apiKey,
'X-Signature': signString(body, signatureKey),
};
// Send this body and these headers to POST /settle-payout.
Verifying incoming requests
CyberPay signs the complete webhook payload with the transaction brand's key and sends the result in X-Signature. This uses the body-signature format for both pay-ins and payouts, including card transactions.
Verify the original, unmodified UTF-8 JSON body with verifyStringSignature(rawBody, receivedSignature, signatureKey). CyberPay sends the same compact JSON used to calculate the signature. If your Node.js handler has already parsed the body, use JSON.stringify(payload) before changing any values or selecting fields. Avoid reserializing it in a way that changes property order, escaping, or number formatting.
HTTP header names are case-insensitive; Node.js exposes this header as headers['x-signature']. Reject a missing or invalid signature before processing the notification. Do not log the secret key or the expected signature.
Responses from the body-signed endpoints above use the same verification procedure with the response body. Card v3 responses use the field-based format below instead.
A valid signature does not prove that a notification is new or that a payment completed. Handle duplicate notifications and inspect the transaction status, as described in Webhooks.
Card v3 signatures
For card POST /v3/payment, send the request signature in Signature. Sign this string, without JSON quotes or a trailing newline:
paymentOptionId:merchantReference:currency:amount
Use an empty segment when merchantReference is omitted or null. Use the numeric amount's JavaScript string representation: an amount of 50.00 signs as 50. Keep the currency and reference exactly as submitted.
For a signed card v3 business response, read X-Signature and build the string from the nested result object:
result.transactionId:result.merchantReference:result.currency:result.amount:result.status
Use the values returned in that response, including its merchant reference, amount, and status. These signatures cover the listed fields only. Card webhooks still use the complete JSON body, as described above.
See the card signature examples for concrete request and response strings.