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.
The event
Section titled “The event”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.
Verify every delivery
Section titled “Verify every delivery”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.
Handle deliveries idempotently
Section titled “Handle deliveries idempotently”- Expect duplicates. A delivery may arrive more than once; key your handling
on
tx_id+stateand 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
confirmedbefore abroadcastyou missed, you still have the truth. - Respond fast. Acknowledge with
2xximmediately 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
getTransactionon a schedule to catch a missed delivery.
Terminal states
Section titled “Terminal states”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.