# Infinite Giving Webhooks

Infinite Giving (infinitegiving.com) is a non-cash donation platform (stock, crypto, DAF) for
nonprofits. Integration partners receive a webhook whenever a donation is created or updated for a
connected organization.

> Infinite Giving webhooks are for integration partners. Paste the signing secret Infinite Giving gives you at partner kickoff.

## When to Use This Skill

* How do I receive Infinite Giving webhooks?
* How do I verify the `X-IG-Signature` header?
* Why is my Infinite Giving webhook signature verification failing?
* How do I handle `donation:created` and `donation:updated` events?
* How do I detect that a stock or crypto donation completed?

## Verification (core)

Infinite Giving signs `{X-IG-Timestamp}.{raw body}` with HMAC-SHA256 and sends the lowercase hex
digest in `X-IG-Signature` as `sha256=<hex>`. The key is your signing secret used as plain UTF-8
(not hex- or base64-decoded). Verify the raw bytes before parsing any JSON.

```javascript
const crypto = require('crypto');

function verifyInfiniteGiving(rawBody, timestamp, signature, secret) { // rawBody: Buffer
  if (!timestamp || !signature || !secret) return false;
  const hex = signature.replace(/^sha256=/, '').toLowerCase(); // prefix optional
  if (!/^[0-9a-f]{64}$/.test(hex)) return false;               // validate before decoding
  const expected = crypto.createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]))
    .digest();
  return crypto.timingSafeEqual(expected, Buffer.from(hex, 'hex'));
}
// Missing, malformed, or mismatched signature -> respond 401

```

```python
import hashlib, hmac, re

def verify_infinite_giving(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if not timestamp or not signature or not secret:
        return False
    digest = signature.removeprefix("sha256=").lower()
    if not re.fullmatch(r"[0-9a-f]{64}", digest):
        return False
    signed = timestamp.encode("latin-1") + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).digest()
    return hmac.compare_digest(expected, bytes.fromhex(digest))

```

Key points:

* **Raw body.** Never `JSON.parse`/`JSON.stringify` (or decode and re-encode) before verifying; the
  signature covers the exact bytes received. In Express use `express.raw({ type: '*/*' })`.
* **Header value as-is.** Use the `X-IG-Timestamp` value exactly as received (Unix seconds); don't trim or reformat it.
* **Prefix optional.** Accept `X-IG-Signature` with or without the `sha256=` prefix; compare hex case-insensitively.
* **No timestamp window by default.** Retries and Hookdeck replays resend the original headers, so
  rejecting old timestamps would drop legitimate deliveries. The examples offer an opt-in
  `INFINITE_GIVING_WEBHOOK_TOLERANCE_SECONDS` (unset or `0` = off) that checks `X-IG-Timestamp` (Unix seconds) against the clock.

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

## Request Format

`POST` with a JSON envelope. Dispatch on the body's `event` field:

| Header | Description |
| --- | --- |
| `X-IG-Signature` | `sha256=<hex>` HMAC-SHA256 signature |
| `X-IG-Timestamp` | Unix timestamp (seconds) used in the signature |
| `X-IG-Event` | Event type, e.g. `donation:created` |
| `X-IG-Webhook-Id` | Unique delivery id (same as the envelope `id`); use it to dedupe |

```json
{ "id": "A1B2C3D4E", "event": "donation:created", "created_at": "2026-06-23T21:00:00.000Z", "data": { "id": "DON1234567", "object": "donation", "status": "pledged" } }

```

## Common Event Types

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `donation:created` | A donation record is created | Record a pledged gift, notify the nonprofit |
| `donation:updated` | Status or material fields change (`processing`, `complete`, `failed`, `canceled`) | Mark a gift complete, reconcile the sale proceeds |

There is no `donation:completed` event: completion arrives as `donation:updated` with `data.status` set to `"complete"`. Full details in [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/infinite-giving-webhooks/references/overview.md).

## Delivery

Up to 5 attempts: the first delivery plus four retries about 1m, 5m, 30m, and 2h apart. The docs state no timeout, so respond with a 2xx quickly and process asynchronously. Make handlers idempotent (key on `X-IG-Webhook-Id` / envelope `id`).

## Environment Variables

```bash
INFINITE_GIVING_WEBHOOK_SECRET=your_partner_signing_secret   # given at partner kickoff, used as UTF-8
INFINITE_GIVING_WEBHOOK_TOLERANCE_SECONDS=0                  # optional, 0/unset = no timestamp check

```

## Local Development

```bash
npx hookdeck-cli listen 3000 infinite-giving --path /webhooks/infinite-giving

```

No account required: the CLI creates a guest account, a public tunnel, and a web UI for inspecting requests.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/infinite-giving-webhooks/references/overview.md) - Events, headers, and payload structure
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/infinite-giving-webhooks/references/setup.md) - Partner onboarding and signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/infinite-giving-webhooks/references/verification.md) - Signature scheme details and gotchas