Skip to main content
Webhooks let you receive real-time notifications when things happen in CharityStack. When a subscribed event occurs — a new donation, a subscription cancellation, a form update — CharityStack sends an HTTP POST request to a URL you specify, with a JSON payload describing the event. This lets you react immediately: update your CRM, trigger a thank-you email, sync records to an external database, or run any other workflow without polling the API.

The webhook object

The following fields are returned by GET /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.
Only the multi-currency fields are shown in this example; the record’s other fields are sent as stored.

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 the data of these events may include them:
  • donation.created and donation.updated
  • subscription.created, subscription.updated and subscription.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, grossAmount and totalCharged. 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 usual subscription.updated without them. No new event type is added.
  • subscription.payment_method_updated carries 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 the PUT /v1/subscriptions/{id} response) never include these fields. The deprecated /v1/donations endpoints may, because they return the stored donation record.
  • Number format. displayAmount and fxRate are JSON numbers, never strings. A whole amount arrives without decimals (40, not 40.00); otherwise as a decimal (40.25). fxRate may 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 the subscription.created, subscription.updated and subscription.cancelled webhooks may carry it as one object, lastChargeFx:
  • Display only; every charge, refund and payout stays in USD. lastChargeFx is 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 lastChargeFx updates the subscription record, so it arrives as the usual subscription.updated. No new event type is added.
  • Independent of the five. lastChargeFx describes a charge that already happened, so it is not removed when the subscription’s amount changes: a subscription.updated can carry lastChargeFx without the five top-level fields. lastChargeDonorForeignExchangeAt can appear on its own in the rare case the stored snapshot is not valid; ignore it when lastChargeFx is 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/donations endpoints (renewal snapshots are kept on the subscription record only).
  • Retirement. lastChargeFx and lastChargeDonorForeignExchangeAt belong 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.
Changing a subscription’s amount through the API? See how amount changes affect these fields.

Registering a webhook

Call POST /v1/webhooks with a destination URL and the list of events you want to receive.
The webhook secret is included in the creation response and is never shown again. Copy it to a secure location — such as your environment variables or secrets manager — before closing the response. If you lose it, you must delete the webhook and create a new one.

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.
The webhook verification guide includes ready-to-use code examples in multiple languages.

API endpoint

Create a webhook

Register a new webhook endpoint and subscribe to one or more event types.