Webhooks
Overview

Webhooks

Knit delivers a signed POST to your endpoint whenever something you care about changes state. Webhooks are the intended way to track collections and payouts — polling is a fallback for reconciliation, not a primary integration.

Configure your endpoint

Set your webhook URL and secret in the dashboard under Business → Developer → Webhooks. There is no public API for changing them.

Your endpoint must be:

  • Publicly reachable over http:// or https://. Private, loopback, and internal addresses are rejected.
  • Fast. Deliveries time out after 10 seconds. Verify the signature, enqueue the work, and return 2xx immediately.
  • Idempotent. The same event can arrive more than once.

Collections and blockchain notification subscriptions can each carry their own merchantCallbackUrl / webhookUrl, which takes precedence over the business-level URL for that resource's events. Everything else goes to the business-level URL.

Verifying the signature

Every delivery carries an X-Signature header: the HMAC-SHA256 of the raw request body, keyed with your webhook secret, hex-encoded.

HeaderValue
Content-Typeapplication/json
Acceptapplication/json
X-SignatureHex-encoded HMAC-SHA256 of the raw body
Node.js
import crypto from "node:crypto";
 
// `rawBody` must be the unparsed body. Re-serialising the parsed JSON will
// produce a different byte sequence and the signature will not match.
function verify(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
 
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureHeader ?? "", "utf8");
 
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib
import hmac
 
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")
⚠️

Capture the raw body before any JSON middleware parses it. Signing a re-serialised object is the single most common cause of "the signature never matches".

Always compare with a constant-time function, and reject the request if the header is missing.

Retries

A delivery counts as failed if your endpoint returns a non-2xx status, times out, or is unreachable. Knit then retries up to 3 times with a fixed backoff:

AttemptSent
InitialImmediately
Retry 11 minute after the failure
Retry 25 minutes after that
Retry 330 minutes after that

After the third retry, delivery stops. The attempt history — status code, response body, and failure reason — is visible in the dashboard, where you can also trigger a manual resend.

⚠️

The full automatic retry window is roughly 36 minutes, not hours. If your endpoint is down longer than that, reconcile by reading the affected resources back from the API rather than waiting for a redelivery.

Near-identical events for the same business within a five-minute window are collapsed into a single delivery, so a duplicated upstream signal does not become two webhooks. That is a safety net, not a guarantee — keep your handler idempotent.

Payload shape

Every payload carries an eventType. Two things vary by event, so check the table before you write your parser:

  • Envelope. Most events are wrapped in a top-level body object. Two are not.
  • Key casing. Some payloads use snake_case, others camelCase.
EventEnvelopeKey casing
COLLECTION_CREATEDbody wrappersnake_case
COLLECTION_CONFIRMEDbody wrappersnake_case
COLLECTION_SUCCESSFULbody wrappersnake_case
COLLECTION_FAILEDbody wrappersnake_case
PAYOUT_SUCCESSFULbody wrappercamelCase
WALLET_FUNDING_SUCCESSFULtop levelsnake_case
BLOCKCHAIN_TRANSACTION_DETECTEDtop levelcamelCase
⚠️

Webhook payloads do not follow the camelCase convention that API responses do — the transformation applied to API responses is not applied to outbound webhooks. Read each event's page for its exact keys rather than assuming they match the equivalent GET response.

A dispatcher that handles both shapes:

Handling both envelopes
app.post("/webhooks/knit", (req, res) => {
  if (!verify(req.rawBody, req.get("X-Signature"), process.env.KNIT_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
 
  // Acknowledge first — the delivery times out after 10 seconds.
  res.sendStatus(200);
 
  const payload = req.body.body ?? req.body;
  enqueue(payload.eventType, payload);
});

Events