Kite

Webhooks

Get told when payments settle, and check the message really came from your server.

A webhook is a URL on your backend that Kite sends events to: every transfer status change, deposit, recovery and excess refund in an app.

Adding a webhook

Inside an app, open Webhooks and click New webhook. Give it a label and your endpoint's URL, choose which events it gets, and how hard to retry.

Adding a webhook: label, endpoint URL, events and retries

Copy the signing key it shows: you need it to verify deliveries, and it's shown once.

The Webhooks page, with each webhook's events and deliveries

What's sent

Each delivery is a POST with a JSON body:

{
  "id": "4f1c…",
  "eventId": "9b2e…",
  "webhookId": "71d0…",
  "category": "Transfer",
  "previous": { "status": "Processing" },
  "current": { "status": "Settled" },
  "data": {
    "id": "7f3c2a10-…",
    "status": "Settled",
    "payinAddress": "0x…",
    "settlementBlockchainHash": "0x…",
    "quote": {
      "sourceAmount": "49000000",
      "sourceToken": { "symbol": "USDC", "decimals": 6 },
      "customer": null,
      "paymentLink": {
        "id": "d097df9a-…",
        "title": "Pro plan",
        "reference": "order-2041",
        "meta": null
      }
    }
  }
}
FieldDescription
idThis delivery. Each retry has a new one.
eventIdThe event. The same across retries: use it to ignore repeats.
categoryTransfer, Deposit, Recovery or ExcessRefund.
previous, currentThe status before and after.
dataFor transfers, the transfer with its quote, the quote's tokens, customer and payment link.

And these headers:

HeaderValue
X-Kite-SignatureHex HMAC-SHA256 of t=<timestamp>&<raw body>, keyed with the signing key.
X-Kite-TimestampUnix seconds when it was sent.
X-Kite-Event-IdThe event's ID.

Verifying with the SDK

Pass the body exactly as it arrived, before any JSON parsing:

import express from "express";
import { verifyWebhook } from "@kite-01/sdk";

app.post(
"/kite/webhooks",
express.raw({ type: "application/json" }),
async (req, res) => {
const event = await verifyWebhook({
body: req.body,
headers: req.headers,
secret: process.env.KITE_WEBHOOK_SECRET!,
});

    if (event.category === "Transfer" && event.current.status === "Settled") {
      const transfer = event.data as {
        quote: { paymentLink: { reference: string | null } | null };
      };
      await fulfil(transfer.quote.paymentLink?.reference);
    }

    res.sendStatus(200);

},
);

verifyWebhook throws a WebhookVerificationError when the signature doesn't match or the delivery is more than 5 minutes old (tolerance changes that). Answer with a 4xx then.

Verifying without the SDK

import hashlib, hmac, time

def verify(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), f"t={timestamp}&".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Retries

Answer with any 2xx within the webhook's timeout to acknowledge a delivery. Anything else is retried, up to the webhook's maximum retries, with its retry interval between attempts. Every attempt, with the response your endpoint gave, is listed under the webhook in the dashboard.

Do slow work after answering. Acknowledge first, then fulfil the order from a queue, keyed by eventId so a retry never fulfils twice.

On this page