Verifying Webhook Signatures
Axiym signs webhook deliveries so your receiver can verify that the request was sent by Axiym and that the body was not changed in transit. Verify the signature before trusting or processing the event payload.
What is signed
Axiym signs the raw HTTP request body bytes for the webhook POST request.
Do not parse the JSON and then re-serialize it for verification. Changes to whitespace, field order, or encoding will produce a different byte sequence and the signature check will fail.
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"timestamp": "2026-06-23T14:05:09Z",
"type": "withdrawal.completed",
"data": {
"withdrawalId": "9b4e2a1c-6d3f-4a8e-bc7d-1f2a3b4c5d6e",
"status": "COMPLETED"
}
}Signature headers
Webhook deliveries include these headers:
| Header | Description |
|---|---|
X-Signature | Base64-encoded Ed25519 signature of the raw request body. |
X-Key-Id | Public key identifier used to verify the signature. |
X-Algorithm | Signature algorithm. Expected value: Ed25519. |
If any of these headers are missing, reject the webhook and do not process the payload.
Retrieve the public key
Use the X-Key-Id header value to retrieve the verification key:
GET /webhooks/public-keys/{publicKeyId} HTTP/1.1
Authorization: Bearer <access_token>
X-Request-Id: 9b833995-54fd-4a05-8e74-455c1473875aThe API returns a PublicKey object.
{
"publicKeyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"active": true,
"algorithm": "ED25519",
"publicKey": "MCowBQYDK2VwAyEA...",
"createdAt": "2026-06-23T14:05:09Z"
}Cache public keys by publicKeyId and fetch again when you see a new key ID.
This allows Axiym to rotate signing keys without changing your webhook
receiver.
Verification steps
- Capture the raw request body bytes before JSON parsing.
- Read
X-Signature,X-Key-Id, andX-Algorithm. - Reject the request unless
X-AlgorithmisEd25519. - Fetch or load the cached public key for
X-Key-Id. - Verify
X-Signatureagainst the raw request body using the public key. - If verification succeeds, parse the JSON body and process the event.
- De-duplicate by the event
idbefore applying business logic.
Return a non-2xx response, such as 401 Unauthorized, when signature
verification fails. Axiym treats non-2xx responses as failed deliveries and will
retry according to the webhook retry policy.
JavaScript example
This example assumes rawBody is a Buffer containing the exact bytes received
from Axiym and publicKeyResponse is the response from
GET /webhooks/public-keys/{publicKeyId}.
import { createPublicKey, verify } from "node:crypto";
function getHeader(headers, name) {
return headers[name] ?? headers[name.toLowerCase()];
}
export function verifyAxiymWebhook(rawBody, headers, publicKeyResponse) {
const signature = getHeader(headers, "X-Signature");
const keyId = getHeader(headers, "X-Key-Id");
const algorithm = getHeader(headers, "X-Algorithm");
if (!signature || !keyId || algorithm !== "Ed25519") {
return false;
}
if (publicKeyResponse.publicKeyId !== keyId) {
return false;
}
const publicKey = createPublicKey({
key: Buffer.from(publicKeyResponse.publicKey, "base64"),
format: "der",
type: "spki",
});
return verify(
null,
rawBody,
publicKey,
Buffer.from(signature, "base64"),
);
}Python example
This example uses cryptography and assumes raw_body is a bytes value
containing the exact request body.
import base64
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.serialization import load_der_public_key
def get_header(headers, name):
return headers.get(name) or headers.get(name.lower())
def verify_axiym_webhook(raw_body, headers, public_key_response):
signature = get_header(headers, "X-Signature")
key_id = get_header(headers, "X-Key-Id")
algorithm = get_header(headers, "X-Algorithm")
if not signature or not key_id or algorithm != "Ed25519":
return False
if public_key_response["publicKeyId"] != key_id:
return False
public_key = load_der_public_key(
base64.b64decode(public_key_response["publicKey"])
)
try:
public_key.verify(base64.b64decode(signature), raw_body)
return True
except InvalidSignature:
return FalseOperational checks
- Validate the event
timestampand reject events outside your accepted clock skew window. - Process each event
idonce. Webhook delivery is at-least-once, so duplicate deliveries can occur. - Return
2xxonly after the event has been safely persisted or queued. - Fetch the current resource from the API if your system needs the latest authoritative state before taking action.