Webhook Signatures
Verify that webhook deliveries are authentic and unmodified
Every webhook delivery includes two headers that you use to verify the request:
| Header | Contents |
|---|---|
x-bitso-webhook-event-signature | Base64-encoded RSA-SHA256 signature of the payload object. |
x-bitso-key-id | Identifier 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
- Read
x-bitso-webhook-event-signatureandx-bitso-key-id. - Fetch keys from List webhook public keys if the id is not already cached.
- Take the
payloadobject from the JSON body and serialize it as compact JSON (separators=(',', ':')in Python). - Decode the signature from Base64.
- 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. Onlypayloadis 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-idis unknown.
Updated about 2 months ago
