CPN

How-to: Verify Webhook Signatures

Every webhook notification sent by CPN is digitally signed with an asymmetric key. This guide demonstrates how to use the key and signature to verify that a webhook notification was sent by Circle. Validating webhooks in this way can reduce the risk of person-in-the-middle attacks on your subscriber endpoint.

Use the following steps to verify the Circle signature on a webhook notification.

Every webhook notification is digitally signed with an asymmetric key. The asymmetric key is random for each webhook, so you must perform this full authentication flow to validate the key. This signature is available in the header of the message. Each message contains the following headers:

  • X-Circle-Signature: the digital signature generated by Circle
  • X-Circle-Key-Id: the public key ID in UUID format

Extract those values from the header of the webhook message.

Using the X-Circle-Key-Id value, query the /v2/cpn/notifications/publicKey/{keyId} endpoint.

Shell
curl --request GET \
     --url "https://api.circle.com/v2/notifications/publicKey/${PUBLIC_KEY_ID}" \
     --header "Accept: application/json" \
     --header "authorization: Bearer ${YOUR_API_KEY}"

Response

JSON
{
  "data": {
    "id": "879dc113-5ca4-4ff7-a6b7-54652083fcf8",
    "algorithm": "ECDSA_SHA_256",
    "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg==",
    "createDate": "2023-06-28T21:47:35.107250Z"
  }
}

Use the public key and the specified algorithm from the response in step 2, along with the X-Circle-Signature value, to verify the integrity of the webhook's payload.

The following Python code demonstrates how to verify the X-Circle-Signature value:

Python
import base64
import json

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec

# Load the public key from the base64 encoded string
public_key_base64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg=="
public_key_bytes = base64.b64decode(public_key_base64)
public_key = serialization.load_der_public_key(public_key_bytes)

# Load the signature you want to verify
signature_base64 = "MEQCIBlJPX7t0FDOcozsRK6qIQwik5Fq6mhAtCSSgIB/yQO7AiB9U5lVpdufKvPhk3cz4TH2f5MP7ArnmPRBmhPztpsIFQ=="
signature_bytes = base64.b64decode(signature_base64)

# Load and format the message you want to verify
message = "{\"subscriptionId\":\"00000000-0000-0000-0000-000000000000\",\"notificationId\":\"00000000-0000-0000-0000-000000000000\",\"notificationType\":\"webhooks.test\",\"notification\":{\"hello\":\"world\"},\"timestamp\":\"2024-01-26T18:22:19.779834211Z\",\"version\":2}"
message_bytes = message.encode(encoding="utf-8")

# Verify the signature
try:
    public_key.verify(
        signature_bytes,
        message_bytes,
        ec.ECDSA(hashes.SHA256())
    )
    print("Signature is valid.")
except InvalidSignature:
    print("Signature is invalid.")
Did this page help you?
© 2023-2025 Circle Technology Services, LLC. All rights reserved.