Gareth Wilson Gareth Wilson

Guide to Infinite Giving Webhooks: Features and Best Practices

Published


Infinite Giving webhooks tell a partner platform when a nonprofit receives a non-cash gift: a donor pledges shares of stock, the shares are sold, the gift completes, or it fails. If your product embeds Infinite Giving so the nonprofits you serve can accept stock, crypto and donor-advised fund donations, webhooks are how those donations reach your donor records and reporting.

This guide covers how Infinite Giving's partner webhooks are configured and connected, the donation events and their payload, how to verify a delivery, and the practices that keep an Infinite Giving receiver reliable.

What are Infinite Giving webhooks?

Infinite Giving's webhooks are for integration partners, not for individual nonprofits. Your company gets an Infinite Giving account and goes through a partnership kickoff with Infinite Giving's team. At kickoff, Infinite Giving configures your OAuth client and your webhook delivery: you supply the webhook URL, and they give you the signing secret. You don't set up either through the API.

Events start flowing for a nonprofit once it is connected to your partner app. The nonprofit's admin authorises your app through OAuth, and your backend calls the partner-subscriptions endpoint for that organisation. Organisations that sign up through your referral link may be connected automatically.

Every delivery is a JSON POST to the one webhook URL configured for your OAuth client, whichever nonprofit it concerns. Each carries X-IG-Timestamp and X-IG-Signature, an HMAC-SHA256 of the timestamp, a dot and the raw body in the form sha256=<hex>.

Infinite Giving webhook features

FeatureDetails
AvailabilityPlatform partners only, configured at partnership kickoff
ConfigurationWebhook URL and signing secret set by Infinite Giving per OAuth client and environment
ConnectionPOST /v1/organizations/:organization_id/partner-subscriptions per nonprofit
Eventsdonation:created, donation:updated
Signature headerX-IG-Signature: sha256=<hex>
Timestamp headerX-IG-Timestamp, Unix seconds
Other headersX-IG-Webhook-Id (the delivery ID, same as the envelope id), X-IG-Event
SchemeHMAC-SHA256, hex
Signed content<X-IG-Timestamp>.<raw body>
SecretShared out-of-band at kickoff, separate for sandbox and production
RetriesUp to 5 attempts in total, roughly 1 minute, 5 minutes, 30 minutes and 2 hours apart
RedeliveryPOST /v1/deliveries/:id/redeliver
Replay window, source IPsNot documented

Common events

EventFires when
donation:createdA donation record is created
donation:updatedStatus or material fields change after creation, for example to processing, complete, failed or canceled

There is no donation:completed event. A finished gift arrives as donation:updated with status: "complete".

Both events share an envelope and the same donation snapshot in data. Here is Infinite Giving's donation:updated example for a completed stock gift, trimmed:

{
  "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",
    "campaign": { "id": "CAM1234567", "name": "Annual Fund" },
    "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
    },
    "donor": { "first_name": "Jane", "last_name": "Donor", "email": "jane@example.com" },
    "updated_at": "2026-06-24T14:30:00.000Z"
  }
}

A few fields need care. recognition is how the donor wants to be credited (public, private or anonymous), not a name. source means different things by type: the donor's brokerage for stock, the Stripe PaymentIntent ID for currency, and the wallet address for crypto. Gross sale proceeds are fill_price * fill_quantity minus the fee fields, and that won't equal tax_deductible_amount, which for stock is valued on the day the shares were received. tax_deductible_amount is set from creation for currency donations, but stays null for stock, crypto and DAF donations until they are valued.

Setting up Infinite Giving webhooks

  1. Contact Infinite Giving to start partner onboarding. Give them your HTTPS webhook URL and your OAuth callback URL.
  2. At kickoff, receive your OAuth client ID and secret and your webhook signing secret. Store the signing secret server-side, for example as INFINITE_GIVING_WEBHOOK_SECRET. You normally get sandbox credentials first and a separate set for production.
  3. When a nonprofit authorises your app, read its ID from ig_organizations in the token response and connect it:
curl -X POST "https://api.infinitegiving.com/v1/organizations/$ORGANIZATION_ID/partner-subscriptions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

The call needs a token with the webhooks:write scope, and it is safe to retry. DELETE /v1/partner-subscriptions/:id stops delivery for that organisation, and connecting again restores it, so you can use it to pause.

If you receive Infinite Giving webhooks through Hookdeck, create a source with the Infinite Giving type and give its URL to Infinite Giving as your webhook URL. Infinite Giving webhooks are for integration partners, so paste the signing secret Infinite Giving gives you at partner kickoff into the source. The source accepts POST only, which is the method Infinite Giving uses. Because sandbox and production have separate secrets, use a separate source for each.

Securing Infinite Giving webhooks

Read X-IG-Timestamp and X-IG-Signature. Build the signed bytes from the timestamp exactly as received, a literal ., and the raw request body. Compute an HMAC-SHA256 of them with the signing secret as a UTF-8 key, hex-encode it, and compare it with the digest after sha256= in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyInfiniteGivingSignature(rawBody, signatureHeader, timestampHeader, secret, toleranceSeconds = 0) {
  if (!signatureHeader || !timestampHeader || !secret) return false;

  // The header is "sha256=<hex>". Accept a bare digest too.
  const digest = signatureHeader.trim().replace(/^sha256=/, '');
  if (!/^[0-9a-fA-F]{64}$/.test(digest)) return false; // check before Buffer.from(x, 'hex')

  // Optional replay window, off by default. Retries and replays resend the original timestamp.
  if (toleranceSeconds > 0) {
    if (!/^\d+$/.test(timestampHeader)) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > toleranceSeconds) return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(`${timestampHeader}.`, 'utf8'), rawBody]))
    .digest();
  const received = Buffer.from(digest, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/infinite-giving', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.INFINITE_GIVING_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const valid = verifyInfiniteGivingSignature(
    req.body,
    req.get('x-ig-signature'),
    req.get('x-ig-timestamp'),
    secret
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id (also X-IG-Webhook-Id), then upsert event.data by its id.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re
import time


def verify_infinite_giving_signature(raw_body: bytes, signature_header, timestamp_header, secret: str,
                                     tolerance_seconds: int = 0) -> bool:
    if not signature_header or not timestamp_header or not secret:
        return False
    digest = signature_header.strip().removeprefix("sha256=")
    if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):
        return False
    # Optional replay window, off by default.
    if tolerance_seconds > 0:
        if not (timestamp_header.isascii() and timestamp_header.isdigit()):
            return False
        if abs(time.time() - int(timestamp_header)) > tolerance_seconds:
            return False
    signed = timestamp_header.encode("utf-8") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(digest.lower().encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_infinite_giving_signature(raw_body, request.headers.get("x-ig-signature"),
#                                  request.headers.get("x-ig-timestamp"),
#                                  os.environ["INFINITE_GIVING_WEBHOOK_SECRET"])

Infinite Giving's own sample compares the strings with ===, which isn't constant-time. Use crypto.timingSafeEqual or hmac.compare_digest as above. Infinite Giving also says to use "the raw request body bytes" and not to re-serialise the JSON first.

Infinite Giving doesn't document a tolerance window for X-IG-Timestamp. The samples above leave the check off unless you pass a tolerance. Hookdeck applies no timestamp tolerance, and its retries and replays resend the original header, as does Infinite Giving's own retry of the same delivery, so a short window would reject legitimate deliveries.

Make Infinite Giving webhooks production-ready. Hookdeck Event Gateway verifies the X-IG-Signature header over the timestamp and raw body, stores every donation event, and delivers it to your service with retries you control.

Infinite Giving webhook limitations and pain points

The URL and secret are set by Infinite Giving

The Problem: Moving your webhook endpoint, or adding a second consumer, means going back to Infinite Giving.

Why It Happens: The webhook URL and signing secret are configured for your OAuth client at kickoff. Subscriptions carry neither, so there is no API call to change them.

Workarounds:

  • Choose a stable URL for kickoff, ideally one you can re-point without involving Infinite Giving.
  • Keep the signing secret in a secret manager, separately for sandbox and production.

How Hookdeck Can Help: Give Infinite Giving a Hookdeck source URL at kickoff. Moving your service, or fanning donation events out to several services, then becomes a change to Hookdeck connections rather than a request to Infinite Giving.

One URL receives every nonprofit's donations

The Problem: Your receiver has to work out which customer each donation belongs to.

Why It Happens: Delivery always uses the single webhook URL for your OAuth client. Each nonprofit is identified only in the body, by data.organization.id.

Workarounds:

  • Map data.organization.id to your own customer record when you connect the organisation.
  • Log and alert on organisation IDs you don't recognise.

How Hookdeck Can Help: Hookdeck filters can route on data.organization.id, so large customers or separate environments can go to their own destinations.

Retries stop after about two and a half hours

The Problem: A longer outage on your side loses donation updates.

Why It Happens: Infinite Giving makes up to 5 attempts in total, the last roughly 2 hours after the one before. After that, the delivery is only in the delivery history.

Workarounds:

  • Use GET /v1/partner-subscriptions/:id/deliveries after an outage and POST /v1/deliveries/:id/redeliver for anything you missed. This needs the webhooks:write scope.
  • Fetch the donation from the API before acting on a status change you may have missed in between.

How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives and stores it, then delivers to your service with retries you configure, so your own downtime doesn't depend on Infinite Giving's retry schedule. Hookdeck answers Infinite Giving with a 200 either way, and records any request that fails the signature check as failed verification, so it never reaches your destination.

Amounts arrive in stages

The Problem: Reports show null or unexpected donation amounts.

Why It Happens: For stock, crypto and DAF gifts, tax_deductible_amount is null until the donation is valued, and the sale proceeds in trade differ from the tax-deductible value.

Workarounds:

  • Treat donation:updated as the source of truth, and update amounts as they arrive.
  • Keep tax-deductible value and sale proceeds as separate fields in your own records.

How Hookdeck Can Help: Hookdeck keeps every delivery, so you can trace how a donation's fields changed from pledged to complete.

Best practices

Verify against the raw body

The signature covers the exact bytes Infinite Giving sent. In Express, use express.raw({ type: '*/*' }). In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, use await request.body().

Deduplicate on X-IG-Webhook-Id

Infinite Giving says "the same delivery may be sent more than once" and recommends X-IG-Webhook-Id, which matches the envelope id, as the idempotency key. See our guide to webhook idempotency.

Upsert donations by ID

Every event carries a full snapshot, so store data by data.id and compare updated_at rather than applying changes in arrival order.

Acknowledge fast and process asynchronously

Infinite Giving asks for a quick 2xx and asynchronous processing. Verify, persist the event, return 200, and update your records afterwards. See why to process webhooks asynchronously.

Conclusion

Infinite Giving sends partners donation:created and donation:updated events for every nonprofit they have connected, all to one URL set at kickoff. Verify X-IG-Signature as a hex HMAC-SHA256 of the timestamp, a dot and the raw body, deduplicate on X-IG-Webhook-Id, treat donation:updated with status: "complete" as completion, and use the delivery history to recover anything missed.

Hookdeck Event Gateway sits in front of your receiver, verifies every Infinite Giving signature, and holds each donation event until your service has processed it.

Get started with Hookdeck for free and handle Infinite Giving webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.