Skip to content

Security ​

Signing your callback ​

Every OOB callback must carry a signature header. The scheme is HMAC-SHA256 over the raw request body, keyed by the shared secret, Base64-encoded:

signature = base64_standard( HMAC_SHA256( raw_request_body, hex_decode(shared_key) ) )

Two details decide whether your signature verifies:

  • The key is a hex string. Decode it to bytes before using it as the HMAC key. Using the hex characters directly as key material produces a valid-looking signature that never matches.
  • Sign the exact bytes you send. Serialise the body once, sign that byte array, and send that same array. Re-serialising between signing and sending — a common result of passing an object to an HTTP client that encodes it again — reorders keys or changes whitespace, and the signature no longer matches the body received.

Go ​

go
func signBody(body []byte, hexKey string) (string, error) {
    key, err := hex.DecodeString(hexKey)
    if err != nil {
        return "", err
    }
    mac := hmac.New(sha256.New, key)
    mac.Write(body)
    return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}

// body must be the same []byte passed to the request:
sig, _ := signBody(body, sharedKeyHex)
req.Header.Set("signature", sig)

Node.js ​

javascript
import { createHmac } from "node:crypto";

function signBody(body, hexKey) {
  return createHmac("sha256", Buffer.from(hexKey, "hex"))
    .update(body)
    .digest("base64");
}

// body must be the same string passed as the request body:
const body = JSON.stringify(payload);
const signature = signBody(body, sharedKeyHex);

Verifying by hand ​

bash
BODY='{"TransactionId":"...","StepupRequestId":"...","Status":"SUCCESS"}'
KEY=<your_shared_key_hex>

SIG=$(printf '%s' "$BODY" \
  | openssl dgst -sha256 -mac HMAC -macopt hexkey:$KEY -binary \
  | base64)

curl -sS -X POST "$OOB_CALLBACK_URL" \
  -H 'Content-Type: application/json' \
  -H "signature: $SIG" \
  -d "$BODY"

Note printf '%s' rather than echo — echo appends a newline, which is part of the body you sign but easy to lose when the request is sent, and the signature will not match.

Inbound requests from the ACS ​

Calls the ACS makes to your platform are not signed at the application layer. They are protected at the network layer: restrict your endpoints to the source IP ranges exchanged during onboarding, and serve them over TLS.

Replay protection ​

The ACS matches every callback against the session's StepupRequestId and its current state. A callback for a session that is already final is ignored, so a replayed request cannot overwrite a recorded outcome.

Cardholder data ​

The full PAN appears in exactly one place: the /step_up request. Nothing else in this integration carries one.

Keep that request inside your cardholder data environment, and keep it out of your logs. If you log request bodies by default, add an exception for this endpoint before you go live — not after.

Key rotation ​

The shared HMAC key is a production credential. Agree a rotation schedule during onboarding and coordinate the change with Cake; both sides must switch together, since there is a single active key.

Developers