Skip to content

Webhooks

Transactions settle asynchronously. Instead of polling getTransaction, register a webhook URL and XKOVA will POST each state change to it — and sign every delivery so you can prove it came from us.

Each delivery is a JSON body:

{
"type": "transaction.confirmed",
"org_id": "org_123",
"tx_id": "tx_9f3a",
"state": "confirmed",
"tx_hash": "0x…",
"ts": 1757030400
}

type is transaction.<state> (e.g. transaction.broadcast, transaction.confirmed, transaction.failed); state is the transaction’s new state, matching the values in Core concepts. ts is Unix seconds.

Each delivery carries an X-Xkova-Signature header: a hex Ed25519 signature over the exact request body. Fetch the public verifying key once from GET /webhook-key (unauthenticated) and check every delivery against it. Reject anything that does not verify — do not act on an unverified body.

import { verify } from "@xkova/mpc";
// Fetch and cache once at startup.
const { public_key } = await fetch(
"https://api.mpc-dev.xkova.com/v1/webhook-key",
).then((r) => r.json());
const webhookKey = Buffer.from(public_key, "hex");
// In your HTTP handler — use the RAW body bytes, not a re-serialized object.
function handle(rawBody: Buffer, headers: Record<string, string>) {
const sig = Buffer.from(headers["x-xkova-signature"], "hex");
if (!verify(webhookKey, rawBody, sig)) {
return; // 400 — reject, do not process
}
const event = JSON.parse(rawBody.toString("utf8"));
// ... act on event
}

Verify against the raw bytes you received. If your framework parses the body into an object, re-serializing it will change the bytes and the signature will not match — capture the raw body before parsing.

  • Expect duplicates. A delivery may arrive more than once; key your handling on tx_id + state and make repeat deliveries a no-op.
  • Order is not guaranteed. Treat each event as “this transaction is now in state X”, not “the next step happened”. If you receive confirmed before a broadcast you missed, you still have the truth.
  • Respond fast. Acknowledge with 2xx immediately and do slow work afterward; a slow handler looks like a failed delivery.
  • Reconcile. Webhooks are the fast path, not the only one — for anything critical, also fetch getTransaction on a schedule to catch a missed delivery.

confirmed/final mean success; failed and rejected are terminal failures carrying an error_code on the transaction (see Errors). Once you see a terminal state you will get no further events for that transaction.