# Verifying signatures

Verify each Ed25519 webhook signature before trusting the event payload.

Product: Payment API
Guides follow API reference 0.1.0 and event reference 0.1.0.
Canonical page: https://docs.axiym.io/payment-api/webhooks/verifying-signatures

Axiym signs the raw HTTP request body. Verify the signature before trusting or
processing the event.

Do not parse and re-serialize JSON for verification. Whitespace, field order,
or encoding changes produce different bytes and cause verification to fail.

## Verification steps

1. Capture the raw 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. Load the cached public key for `X-Key-Id`, or retrieve it from the API.
5. Verify the base64 signature against the raw body.
6. Parse the JSON only after verification succeeds.
7. De-duplicate by the event `id`.

Return a non-2xx response when verification fails.

## JavaScript example

```js
import { createPublicKey, verify } from "node:crypto";

export function verifyAxiymWebhook(rawBody, headers, keyResponse) {
  const signature = headers["x-signature"];
  const keyId = headers["x-key-id"];
  const algorithm = headers["x-algorithm"];

  if (!signature || !keyId || algorithm !== "Ed25519") return false;
  if (keyResponse.publicKeyId !== keyId) return false;

  const publicKey = createPublicKey({
    key: Buffer.from(keyResponse.publicKey, "base64"),
    format: "der",
    type: "spki",
  });

  return verify(
    null,
    rawBody,
    publicKey,
    Buffer.from(signature, "base64"),
  );
}
```

## Operational checks

* Cache keys by `publicKeyId` so signing-key rotation is safe.
* If enforcing a maximum event age, allow for delivery delays and retries as
  well as clock skew. The event timestamp is unchanged across delivery
  attempts; it is not the time of the latest attempt. Agree an acceptable
  window with Axiym rather than assuming a short clock-skew limit.
* Persist event IDs so de-duplication survives restarts.
* Return `2xx` only after durable persistence or queueing.
* Fetch the current payout if ordering or current state matters.
