Webhooks
Infinite Giving sends HTTP POSTs to your webhook URL when donations are created or updated for connected organizations. Setup (OAuth + connect) is covered in Partnerships.
Events
| Event | When it fires |
|---|---|
donation:created |
A donation record is created. |
donation:updated |
Status or material fields change after creation (e.g. processing, complete, failed, canceled). |
There is no separate donation:completed event — completion arrives as donation:updated with status: "complete".
Delivery format
{
"id": "A1B2C3D4E",
"event": "donation:created",
"created_at": "2026-06-23T21:00:00.000Z",
"data": { }
}
Headers
| Header | Description |
|---|---|
X-IG-Webhook-Id |
Unique delivery id (same as envelope id). Use to dedupe. |
X-IG-Event |
Event type, e.g. donation:created. |
X-IG-Timestamp |
Unix timestamp (seconds) used in the signature. |
X-IG-Signature |
sha256=<hex> HMAC signature. |
Payload (data)
Both events use the same donation snapshot:
| Field | Type | Description |
|---|---|---|
id |
string | Donation id |
object |
string | Always "donation" |
organization |
object | { id, name } |
type |
string | null | Donation type (e.g. stock, crypto) |
status |
string | null | e.g. pledged, processing, complete, failed, canceled |
identifier |
string | null | Ticker, asset symbol, or similar |
quantity |
number | null | Units donated |
tax_deductible_amount |
number | null | Tax-deductible amount |
recognition |
string | null | How the donor wishes to be credited: public, private, or anonymous. Not a name. |
memo |
string | null | Memo |
source |
string | null | Meaning depends on type: the donor's brokerage for stock, the Stripe PaymentIntent id for currency, the wallet address for crypto. |
cancelation_reason |
string | null | Set when canceled |
campaign |
object | null | { id, name } |
designation |
object | null | { id, name } |
trade |
object | null | Sale details when available (see below) |
donor |
object | { first_name, last_name, email } |
created_at |
string | null | ISO-8601 |
updated_at |
string | null | ISO-8601 |
trade object
| Field | Type | Description |
|---|---|---|
executed_at |
string | null | ISO-8601 |
settled_at |
string | null | ISO-8601 |
fill_price |
number | null | Price per share of the sale. null until the sale fills. |
fill_quantity |
number | null | Shares sold. null until the sale fills. |
sec_fee |
number | |
postedge_fee |
number | |
liquidity_fee |
number | |
orf_fee |
number | |
nasd_fee |
number | |
misc_fee |
number |
Gross proceeds are fill_price * fill_quantity; subtract the fee fields for the net. This will not
equal tax_deductible_amount, which for a stock donation is valued on the date the shares were
received rather than the date they were sold, and is null until the donation reaches processing.
Timing differs by type: currency donations carry a tax_deductible_amount from creation, while
stock, crypto and DAF donations stay null until valued.
Example: donation:created
{
"id": "A1B2C3D4E",
"event": "donation:created",
"created_at": "2026-06-23T21:00:00.000Z",
"data": {
"id": "DON1234567",
"object": "donation",
"organization": { "id": "ORGABCDEFG", "name": "Example Nonprofit" },
"type": "stock",
"status": "pledged",
"identifier": "AAPL",
"quantity": 10,
"tax_deductible_amount": null,
"recognition": "public",
"memo": null,
"source": "Fidelity",
"cancelation_reason": null,
"campaign": { "id": "CAM1234567", "name": "Annual Fund" },
"designation": null,
"trade": null,
"donor": {
"first_name": "Jane",
"last_name": "Donor",
"email": "jane@example.com"
},
"created_at": "2026-06-23T21:00:00.000Z",
"updated_at": "2026-06-23T21:00:00.000Z"
}
}
Example: donation:updated (complete)
{
"id": "F6G7H8I9J",
"event": "donation:updated",
"created_at": "2026-06-24T14:30:00.000Z",
"data": {
"id": "DON1234567",
"object": "donation",
"organization": { "id": "ORGABCDEFG", "name": "Example Nonprofit" },
"type": "stock",
"status": "complete",
"identifier": "AAPL",
"quantity": 10,
"tax_deductible_amount": 2143.5,
"recognition": "public",
"memo": null,
"source": "Fidelity",
"cancelation_reason": null,
"campaign": { "id": "CAM1234567", "name": "Annual Fund" },
"designation": null,
"trade": {
"executed_at": "2026-06-24T13:00:00.000Z",
"settled_at": "2026-06-24T14:00:00.000Z",
"fill_price": 215.0,
"fill_quantity": 10,
"sec_fee": 0.02,
"postedge_fee": 0,
"liquidity_fee": 0,
"orf_fee": 0,
"nasd_fee": 0,
"misc_fee": 0
},
"donor": {
"first_name": "Jane",
"last_name": "Donor",
"email": "jane@example.com"
},
"created_at": "2026-06-23T21:00:00.000Z",
"updated_at": "2026-06-24T14:30:00.000Z"
}
}
Verify the signature
Compute HMAC-SHA256 of `${timestamp}.${rawBody}` with your signing secret (from kickoff). Compare to X-IG-Signature (sha256= + hex digest).
const crypto = require('crypto')
function verify(secret, timestamp, rawBody, signatureHeader) {
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
return expected === signatureHeader
}
Use the raw request body bytes — do not re-serialize JSON before verifying.
Retries and idempotency
- Respond with
2xxquickly; process asynchronously. - Failed deliveries retry up to 5 times with backoff (~1m, 5m, 30m, 2h, 6h).
- Treat
X-IG-Webhook-Idas an idempotency key — the same delivery may be sent more than once.