Webhook Signatures

Verify that webhook deliveries are authentic and unmodified

Every webhook delivery includes two headers that you use to verify the request:

HeaderContents
x-bitso-webhook-event-signatureBase64-encoded RSA-SHA256 signature of the payload object.
x-bitso-key-idIdentifier of the public key that verifies the signature.

The algorithm is SHA256withRSA (RSA PKCS#1 v1.5 with SHA-256). Bitso signs the payload object only, serialized as compact JSON (no extra whitespace). The outer { "event", "payload" } envelope is not part of the signed bytes.

Fetch the public key

Call List webhook public keys. The response is an array of keys. Select the entry whose key_id matches the x-bitso-key-id header.

public_key is a Base64-encoded X.509 SubjectPublicKeyInfo (DER) RSA public key. Cache keys by key_id and refresh the list when you see an unknown id (keys rotate).

This endpoint is public; it does not require HMAC authentication.

Verify a delivery

  1. Read x-bitso-webhook-event-signature and x-bitso-key-id.
  2. Fetch keys from List webhook public keys if the id is not already cached.
  3. Take the payload object from the JSON body and serialize it as compact JSON (separators=(',', ':') in Python).
  4. Decode the signature from Base64.
  5. Verify the signature over the compact JSON bytes with the matching public key and SHA256withRSA.

Reject the request if verification fails.

import base64
import json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

def payload_bytes(webhook_body: dict) -> bytes:
    return json.dumps(webhook_body["payload"], separators=(",", ":"), ensure_ascii=False).encode("utf-8")

def load_public_key(public_key_b64: str):
    der = base64.b64decode(public_key_b64)
    return serialization.load_der_public_key(der)

def verify_signature(webhook_body: dict, signature_b64: str, public_key_b64: str) -> bool:
    public_key = load_public_key(public_key_b64)
    signature = base64.b64decode(signature_b64)
    try:
        public_key.verify(
            signature,
            payload_bytes(webhook_body),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
        return True
    except Exception:
        return False
⚠️

Common mistakes

  • Signing or hashing the entire { "event", "payload" } object. Only payload is signed.
  • Pretty-printing the payload before verification. Use compact JSON with no spaces around separators.
  • Caching a single public key forever. Re-fetch when x-bitso-key-id is unknown.


Did this page help you?