# CharityStack Webhooks

## When to Use This Skill

* How do I receive CharityStack webhooks?
* How do I verify the `X-Webhook-Signature` header on a CharityStack webhook?
* Why does my CharityStack webhook signature verification fail?
* How do I handle `donation.created` or `subscription.cancelled` events?
* Where do I find the CharityStack webhook secret?

## Setup note

CharityStack shows the webhook secret only once, when you create the webhook. Paste it here.

The secret is the `secret` field of the `POST /v1/webhooks` response. If you lose it, delete the webhook and create a new one. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/charitystack-webhooks/references/setup.md).

## Verification (core)

Every delivery carries three headers:

```
X-Webhook-Signature: sha256=<hex digest>
X-Webhook-Timestamp: <unix seconds>
X-Webhook-ID: <delivery identifier>

```

The signature is the lowercase hex HMAC-SHA256 of `<X-Webhook-Timestamp>.<raw body>`, keyed with the webhook secret's UTF-8 bytes (not hex- or base64-decoded). Build the message as bytes, never decode and re-encode the body, and don't parse the JSON before verifying. Accept the digest with or without the `sha256=` prefix, check it is exactly 64 hex characters, compare in constant time, and respond `401` on any failure.

```javascript
const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();

const digest = signature.startsWith('sha256=') ? signature.slice(7) : signature;
if (!/^[0-9a-fA-F]{64}$/.test(digest)) return false;            // validate before decoding
return crypto.timingSafeEqual(expected, Buffer.from(digest.toLowerCase(), 'hex'));

```

```python
signed = b"".join([timestamp.encode("latin-1"), b".", raw_body])
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()

digest = signature.removeprefix("sha256=")
if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):                # validate before decoding
    return False
return hmac.compare_digest(expected, bytes.fromhex(digest))

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/charitystack-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/charitystack-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/charitystack-webhooks/examples/fastapi/).

CharityStack has no official webhook SDK, so verification is manual in every framework.

### Timestamp tolerance is opt-in

CharityStack's guide suggests rejecting timestamps more than 5 minutes old. The examples do **not** do this by default: Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate retries. Set `CHARITYSTACK_WEBHOOK_TOLERANCE_SECONDS` (for example `300`) to turn it on; unset or `0` means off. `X-Webhook-Timestamp` is Unix seconds.

### Read the raw body

Express: `express.raw({ type: '*/*' })`. Next.js: `await request.arrayBuffer()`. FastAPI: `await request.body()`. Parse the JSON only after the signature check.

## Common Event Types

Dispatch on the body's `type` field.

| Event | Triggered when |
| --- | --- |
| `donation.created` | A new donation is received |
| `donation.updated` | A donation's status changes |
| `subscription.created` | A new recurring subscription is started |
| `subscription.updated` | A subscription is modified (e.g. amount changed) |
| `subscription.cancelled` | A subscription is cancelled |
| `subscription.payment_method_updated` | A payment method is updated through a hosted update link (`subscription.updated` is also emitted) |
| `contact.created` / `contact.updated` | A contact is added or updated |
| `form.created` / `form.updated` | A form is created or its configuration changes |

> **For payload notes and delivery details**, see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/charitystack-webhooks/references/overview.md) and the [CharityStack webhooks docs](https://www.charitystack.com/docs/concepts/webhooks).

## Delivery Notes

* Method: `POST` with a JSON body.
* No retry or timeout policy is documented, so return a `2xx` quickly and process asynchronously. Return `2xx` for event types you don't handle.
* `X-Webhook-ID` identifies a delivery attempt, so don't rely on it alone to deduplicate retries; deduplicate on an identifier from the event payload.

## Environment Variables

```bash
CHARITYSTACK_WEBHOOK_SECRET=your_charitystack_webhook_secret
# Optional: reject timestamps older than this many seconds (unset or 0 = off)
CHARITYSTACK_WEBHOOK_TOLERANCE_SECONDS=300

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 charitystack --path /webhooks/charitystack

```

Register the printed URL as the webhook `url` when you call `POST /v1/webhooks`.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/charitystack-webhooks/references/overview.md) - What CharityStack webhooks are, events, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/charitystack-webhooks/references/setup.md) - Register a webhook and capture the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/charitystack-webhooks/references/verification.md) - Signature details, gotchas, debugging