Axiym

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:

HeaderDescription
X-SignatureBase64-encoded Ed25519 signature of the raw request body.
X-Key-IdPublic key identifier used to verify the signature.
X-AlgorithmSignature 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-455c1473875a

The 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

  1. Capture the raw request body bytes before JSON parsing.
  2. Read X-Signature, X-Key-Id, and X-Algorithm.
  3. Reject the request unless X-Algorithm is Ed25519.
  4. Fetch or load the cached public key for X-Key-Id.
  5. Verify X-Signature against the raw request body using the public key.
  6. If verification succeeds, parse the JSON body and process the event.
  7. De-duplicate by the event id before 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 False

Operational checks

  • Validate the event timestamp and reject events outside your accepted clock skew window.
  • Process each event id once. Webhook delivery is at-least-once, so duplicate deliveries can occur.
  • Return 2xx only 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.