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.
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-SignaturestringHMAC-SHA256 of the raw body, hex-encoded.
X-Wishbone-Delivery-IDuuidStable 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-EventstringEvent name, matching the event field in the body.
X-Wishbone-TimestampintegerUnix timestamp of the delivery attempt.
Events
donation.completedConfirmed by the school. Includes school_confirmation_id.
donation.failedCould not be completed. Includes failure_reason and points_refund_required, so you can reverse the points debit.
donation.pendingReceived and queued, awaiting school-side confirmation.
account.linkedCardholder finished linking.
account.unlinkedCardholder removed the Wishbone connection.
school.deactivatedA 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.