Webhooks

Signed, retried callbacks telling you when a donation settled or failed.

A donation request returns 202 immediately. The real outcome arrives as a webhook once Wishbone has pushed the gift to the school's platform. Register your endpoint during onboarding.

Verifying the signature

Every delivery is signed with HMAC-SHA256 over the raw request body using your webhook secret. Verify before trusting the payload, and compare in constant time.

Node.js
const crypto = require("crypto");

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)          // the raw bytes, before JSON.parse
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Sign over the raw body. If your framework parses JSON before you reach the handler and you re-serialize it, key order or whitespace can differ and the signature will not match.

Headers

X-Wishbone-Signaturestring

HMAC-SHA256 of the raw body, hex-encoded.

X-Wishbone-Delivery-IDuuid

Stable across every retry of the same event. Use it to deduplicate — an attempt that timed out on your side may already have been processed.

X-Wishbone-Eventstring

Event name, matching the event field in the body.

X-Wishbone-Timestampinteger

Unix timestamp of the delivery attempt.

Events

donation.completed

Confirmed by the school. Includes school_confirmation_id.

donation.failed

Could not be completed. Includes failure_reason and points_refund_required, so you can reverse the points debit.

donation.pending

Received and queued, awaiting school-side confirmation.

account.linked

Cardholder finished linking.

account.unlinked

Cardholder removed the Wishbone connection.

school.deactivated

A school is no longer accepting donations.

Payload

{
  "event": "donation.completed",
  "created_at": "2026-05-07T14:22:00Z",
  "data": {
    "donation_id": "9f14c6d2-7b3a-4e51-a0c8-2d6b8e4f1a92",
    "wishbone_user_id": "b3d1f0c2-8a4e-4f77-9c2f-1e5a7d9b0c34",
    "bank_transaction_ref": "TXN-12345",
    "school_id": "sch_tennessee",
    "fund_designation": "annual_fund",
    "dollar_amount": 50.00,
    "points_redeemed": 5000,
    "loyalty_program_id": "lp_chase_sapphire",
    "school_confirmation_id": "TC-98765",
    "status": "completed"
  }
}

Retries

Return any 2xx to acknowledge. Anything else — or a timeout past 10 seconds — is retried on a fixed schedule: 1m → 5m → 30m → 2h → 12h. After the last attempt the delivery is dead-lettered and raises an internal alert.

Acknowledge fast and process asynchronously. A slow handler burns your retry budget. Since X-Wishbone-Delivery-ID is stable across retries, deduplicating on it is safe and is the recommended pattern.