Skip to main content
Webhooks let you receive notifications when payout activity happens — without polling. You subscribe a callback URL to an event type, and Karat sends an HTTP POST to that URL whenever the event occurs.

Subscribe to an event

1

Discover available events

Call GET /events to list the event types you can subscribe to.
2

Create a subscription

Call POST /events/subscription with the event_name and your callback_url. The response includes a secret — store it to verify future deliveries.
3

Acknowledge deliveries

Respond with a 2xx status so Karat knows the event was received.
curl https://payouts.api.trykarat.com/events/subscription \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "payout.updated",
    "callback_url": "https://example.com/api/webhooks/callback"
  }'

Secret generation

Karat generates a unique signing secret when you create a subscription. The secret is returned only once in the create subscription response — store it securely so you can verify deliveries.

Handshake verification

When creating a subscription, the API verifies ownership of the callback URL by performing a challenge-response handshake. Your server must handle this before the subscription is accepted. The API sends a POST request to your callback_url with a JSON body:
{
  "type": "url_verification",
  "challenge": "a3f8b2e..."
}
Your server must respond within 30 seconds with HTTP status 200 and a JSON body echoing the challenge:
{
  "challenge": "a3f8b2e..."
}
If verification succeeds, the subscription is created. If it fails, the API returns a 400 error with one of these messages:
MessageCause
Callback URL verification failed: could not reach the URLThe URL was unreachable, timed out, or the connection was refused
Callback URL verification failed: received status {code}Your server responded with a non-2xx status code
Callback URL verification failed: invalid JSON responseThe response body was not valid JSON
Callback URL verification failed: challenge mismatchThe challenge value in the response did not match the one sent

Event types

EventWhen it fires
payout.updatedA payout’s status changes
tax_form.createdA recipient completes a tax form

Payload

Every delivery shares the same envelope — id, event, created_at, subscription_id, and an event-specific data object.

payout.updated

{
  "id": "a2b6f9c8-7d5f-4c2b-bd7a-1c2f9b5c44b9",
  "event": "payout.updated",
  "created_at": "2026-03-02T12:34:56Z",
  "subscription_id": "7a6b5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d",
  "data": {
    "id": "8e0c87c1-948c-4e57-9b48-9b5ab4c0d7e8",
    "status": "created",
    "reason": null,
    "reference_id": "6ae5b965-ad32-4d21-bff9-f1e661f05ca3",
    "payout_intent_id": "05545ccf-0798-4661-8b36-2ad7c0bf5375",
    "date": "2025-09-30T14:59:37.055Z"
  }
}

tax_form.created

{
  "id": "78ce3584-0761-40ab-bcb1-ef7c5bffe095",
  "event": "tax_form.created",
  "created_at": "2026-03-10T23:45:07.878Z",
  "subscription_id": "48582de1-c2fb-4fd3-bdf3-39a7138d1164",
  "data": {
    "id": "ffc96bee-3ce9-4bfc-a436-c08cc2a3d33e",
    "recipient": {
      "id": "db783652-823b-46f8-88e2-93ae37442232",
      "firstName": "John",
      "lastName": "Doe",
      "nickname": null
    },
    "createdAt": "2026-03-10T23:45:07.868Z",
    "url": "https://url.com/pdf"
  }
}

Verify webhook signatures

Every delivery is signed so you can confirm it came from Karat. Verify the signature before processing the payload. Each request includes these headers:
HeaderDescription
X-Karat-SignatureThe signature, in the format v1=<base64>.
X-Karat-Webhook-TimestampUnix epoch seconds when the event was sent.
X-Karat-Subscription-IdThe subscription the event belongs to — use it to look up the right secret.
X-Karat-Webhook-IdUnique delivery ID, for deduplication.
The signature is an HMAC-SHA256 of timestamp + "." + raw_body, keyed with your subscription secret, then base64-encoded:
signed_payload   = X-Karat-Webhook-Timestamp + "." + raw_request_body
signature_base64 = base64(HMAC_SHA256(secret, signed_payload))
To verify a delivery:
1

Look up the secret

Use X-Karat-Subscription-Id to find the secret you stored when creating the subscription.
2

Check the timestamp

Reject deliveries where X-Karat-Webhook-Timestamp is outside a ±300 second window to protect against replays.
3

Recompute and compare

Recompute the signature from the raw request body and compare it to the value in X-Karat-Signature using a constant-time comparison.
4

Deduplicate

Use X-Karat-Webhook-Id (and the id in the body) to ignore duplicate deliveries.
import crypto from "crypto";

function verifyKaratWebhook({ headers, rawBody }, secret) {
  const timestamp = headers["x-karat-webhook-timestamp"];

  // Replay protection: reject deliveries older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  // Build the signed payload from raw bytes so the body is never re-encoded.
  const signedPayload = Buffer.concat([
    Buffer.from(`${timestamp}.`),
    Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody),
  ]);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("base64");

  const provided = (headers["x-karat-signature"] ?? "").replace(/^v1=/, "");
  const a = Buffer.from(expected);
  const b = Buffer.from(provided);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Compute the signature over the exact raw bytes you received, before any JSON parsing or re-serialization — reformatting the body will change the signature. If verification fails, return a non-2xx response and do not process the event.

Delivery

  • Deliveries are sent asynchronously and retried on failure.
  • Always return a 2xx response quickly; do heavy processing out of band.

Manage subscriptions