The webhook object
The following fields are returned byGET /v1/webhooks and GET /v1/webhooks/{id}.
Available events
Subscribe to any combination of the events below when you register a webhook.When a hosted payment method update link completes, CharityStack emits
subscription.payment_method_updated and also emits subscription.updated for backward compatibility. The dedicated payment method event includes a safe summary and excludes provider identifiers.Event payloads
Every delivery is a JSON body with the same envelope.data is the stored record for the event.
Display-currency fields (multi-currency)
Nonprofits can let donors see amounts in a currency other than USD. When a donor gives with a display currency, the donation record and, for a recurring gift, the subscription record store five extra fields. Webhooks send the stored record, so thedata of these events may include them:
donation.createdanddonation.updatedsubscription.created,subscription.updatedandsubscription.cancelled
- All five or none. They appear only for nonprofits that turned on display currencies, and only on records whose donor gave with a display currency. Not every record of such a nonprofit carries them.
- Display only; every charge, refund and payout stays in USD. So do
amount,grossAmountandtotalCharged. Do not use these fields for accounting. - Amount changes remove them. When a subscription’s charged total changes (in the dashboard, the donor portal or
PUT /v1/subscriptions/{id}), the same update removes all five from the subscription, and the change arrives as the usualsubscription.updatedwithout them. No new event type is added. subscription.payment_method_updatedcarries none of the five. Its payload is a fixed list of payment-method fields.- The REST objects do not change. The Payment and Subscription objects (
/v1/payments,/v1/subscriptions, and thePUT /v1/subscriptions/{id}response) never include these fields. The deprecated/v1/donationsendpoints may, because they return the stored donation record. - Number format.
displayAmountandfxRateare JSON numbers, never strings. A whole amount arrives without decimals (40, not40.00); otherwise as a decimal (40.25).fxRatemay lose precision in parsers that read numbers as floats. - Compatibility. The change is additive. Consumers that reject unknown keys should allow these five, and the two last-renewal fields below.
Last renewal in the display currency
For a recurring gift in a display currency, each renewal charge (in USD, as always) can also be shown to the donor in that currency. The subscription record keeps the most recent one that got a rate, and thesubscription.created, subscription.updated and subscription.cancelled webhooks may carry it as one object, lastChargeFx:
- Display only; every charge, refund and payout stays in USD.
lastChargeFxis an estimate of what the donor saw for that renewal, not what was charged; do not use it for accounting. - Complete or absent. The object always has all five keys. It is left out when the subscription has no valid last-renewal snapshot, for example before the first renewal, or when a renewal could not get a current rate (then the previous renewal’s object stays).
- When it changes. A renewal that records a new
lastChargeFxupdates the subscription record, so it arrives as the usualsubscription.updated. No new event type is added. - Independent of the five.
lastChargeFxdescribes a charge that already happened, so it is not removed when the subscription’s amount changes: asubscription.updatedcan carrylastChargeFxwithout the five top-level fields.lastChargeDonorForeignExchangeAtcan appear on its own in the rare case the stored snapshot is not valid; ignore it whenlastChargeFxis absent. - Where it never appears. Not on
subscription.payment_method_updated, not on the Payment or Subscription REST objects, and not on donation webhooks or the deprecated/v1/donationsendpoints (renewal snapshots are kept on the subscription record only). - Retirement.
lastChargeFxandlastChargeDonorForeignExchangeAtbelong to the same multi-currency display feature as the five fields. If that feature’s fields are ever retired or renamed, these two go with them.
Registering a webhook
CallPOST /v1/webhooks with a destination URL and the list of events you want to receive.
Verifying webhook signatures
Every webhook delivery includes three headers that you should use to authenticate the request before processing the payload.
CharityStack signs each payload by combining the timestamp and raw request body, then computing an HMAC-SHA256 digest using your webhook secret. To verify a request:
1
Extract the headers
Read
X-Webhook-Signature, X-Webhook-Timestamp, and X-Webhook-ID from the incoming request.2
Construct the signed string
Concatenate the timestamp and the raw request body with a
. separator:3
Compute the expected signature
Compute an HMAC-SHA256 of the signed string using your webhook secret as the key, then hex-encode the result.
4
Compare signatures
Compare your computed digest to the value in
X-Webhook-Signature (after stripping the sha256= prefix). Use a constant-time comparison to prevent timing attacks. Reject the request if they do not match.5
Check the timestamp
Optionally reject requests where
X-Webhook-Timestamp is older than a few minutes to protect against replay attacks.API endpoint
Create a webhook
Register a new webhook endpoint and subscribe to one or more event types.