Developer Documentation

Everything you need to integrate with the FundraiserMax API. Build custom workflows, sync donor data, and connect AI assistants to your fundraising platform.

Donations API

The Donations API is the primary surface for moving money in and out of FundraiserMax programmatically. Use it to record contributions captured by external payment processors, sync legacy donation history during a migration, run nightly reconciliation against your accounting system, or build custom dashboards over real-time giving activity.

All donation records are immutable once created — corrections are made by recording an offsetting entry rather than mutating history, which keeps your FEC and IRS audit trail clean. Every endpoint requires a valid API key (see Authentication) and is scoped to a single account via the accountId parameter.

Looking for the no-code option? If you're not building a custom integration, FundraiserMax already imports donations from Stripe, ActBlue, WinRed, CSV files, and most major payment processors automatically. The Donation Management feature covers what's available in the dashboard.

Common use cases

  • Record contributions from a custom donation page. POST to /v1/donations immediately after a successful charge in your payment provider so the donor receives an automated thank-you email and the gift appears on their record.
  • Nightly reconciliation against your accounting system. GET /v1/donations with startDate set to yesterday and compare totals to your ledger. Any discrepancy points to a payment-processor failure or a duplicate import.
  • One-time historical migration. Loop through legacy donations and POST them with the original donationDate. Set notes to something like "Imported from <old system> on 2026-04-01" so the provenance is searchable later.
  • Real-time fundraising dashboards. Combine Dashboard Stats for high-level totals with the Donations list endpoint for per-gift drill-down.

Best practices

Always send the original gift date. When importing donations from a payment processor that batches captures, set donationDate to the date the donor authorized the charge — not the date your script runs. Reports and FEC filings rely on the authorization date.
Idempotency for imports. The POST endpoint does not deduplicate by amount and date alone. Track the external transaction ID in the notes field (for example "stripe:ch_3OqXXXX") and check before re-posting if your import job retries.
Pagination. The list endpoint caps limit at 100. To export a full year of donations, page through with offset in increments of 100 and stop when data.length < limit.

Integration patterns

Most teams arrive at one of three integration shapes. Pick the one that matches your stack and follow the recipe below. Each pattern includes the order of operations, idempotency guidance, and the failure modes worth instrumenting before you go to production.

Pattern 1: Custom checkout charging Stripe directly

You run your own donation page (or embed Stripe Elements on your website) and need every successful charge to appear in FundraiserMax with the donor on the right contact record. This is the most common pattern for political committees with bespoke landing pages and for nonprofits replacing a legacy donation form.

  1. Capture the contact first. Before creating the Stripe PaymentIntent, look up or create the contact in FundraiserMax with the donor's email and address. The donation API requires a contactId, so doing this up-front saves a retry on the post-charge path.
  2. Confirm the charge. Let Stripe (or your processor of choice) handle card collection client-side and confirm the PaymentIntent. Wait for the payment_intent.succeeded webhook rather than the client-side success callback so a closed browser tab does not cost you a donation record.
  3. Record the donation. POST to /v1/donations with the contact ID, the dollar amount, donationDate set to the Stripe charge timestamp, and the Stripe charge ID embedded in notes (e.g. "stripe:ch_3OqXXXX"). The charge ID is what makes the import idempotent: before posting, list donations by this contact for the past 24 hours and skip if the charge ID is already in notes.
  4. Trigger stewardship. Set sendReceipt: true if you want FundraiserMax to email the donor a thank-you and a tax receipt automatically. If you run your own acknowledgment workflow, set it to false and use the donation.created webhook as your trigger instead.
Refunds and disputes. Subscribe to Stripe's charge.refunded and charge.dispute.created webhooks. When they fire, POST a negative-amount donation referencing the original charge ID so your FEC and IRS audit trail shows the reversal as a discrete event rather than a deletion.

Pattern 2: Backfilling history from a legacy CRM

Migrations are one-shot scripts that import every prior donation, then never run again. The hardest part is preserving the original gift date because every fundraising metric (donor lifetime value, RFM segmentation, year-over-year reporting, FEC cycle-to-date totals) anchors on that date.

  1. Export contacts first, donations second. A donation without a contact cannot be posted. Use the Contacts API to bulk-create contacts, store the resulting IDs alongside your legacy donor IDs in a translation table, then loop through donations and resolve each contactId from the table.
  2. Use the original gift date. Set donationDate to the date the donor authorized the gift in your old system, not the date your script runs. If you only have a year and month, set the day to the first of the month and mark it in notes for transparency.
  3. Preserve provenance. Include the legacy donation ID and source system name in notes (e.g. "NationBuilder:donation/123456 imported 2026-04-01"). Six months later when someone asks where a record came from, you will be glad it is searchable.
  4. Throttle and resume. The API enforces rate limits. For large migrations, batch to roughly 5 requests per second, log each successful donation ID, and make the script resumable from the last logged ID. A full year of mid-six-figure donation history typically completes in under an hour.

Pattern 3: Nightly reconciliation against accounting

Finance teams want a closed-loop check between FundraiserMax and the general ledger. This pattern runs every night, pulls the prior day's donations, and reports any discrepancy against QuickBooks, NetSuite, Sage Intacct, or your bank deposit feed.

  1. Pull yesterday's donations. GET /v1/donations with startDate set to yesterday at 00:00 UTC and endDate at 23:59:59. Page until the response returns fewer rows than the limit.
  2. Group by payment method or designation. If your ledger has separate accounts for credit-card receipts, ACH, employer matching, and major gifts, group the FundraiserMax results the same way before comparing.
  3. Diff against the ledger. Any donation in FundraiserMax that is missing in the ledger means your bank import failed or a manual entry was missed. Any ledger entry not in FundraiserMax means a donation was processed outside the platform and needs to be recorded.
  4. Page treasurers, not engineers. When a discrepancy exceeds your tolerance (typically $1 for absolute mismatch, 0.1% for batch totals), send the finance owner a Slack message or email rather than logging to a console no one reads.

Donation fields, conventions, and gotchas

  • Amounts are in dollars, not cents. Unlike Stripe, FundraiserMax stores amounts as decimal dollars ("125.00"). A common bug is sending 12500 intending $125.00 and creating a record for $12,500.
  • Election cycle attribution. Political committees should set electionCycle to the appropriate FEC cycle code (e.g. "P2026" for the 2026 primary). FundraiserMax uses this to enforce contribution limits per cycle and to generate Form 3 schedules.
  • Designations and funds. Set designation to the named fund the donor specified (e.g. "General", "Capital Campaign","Legal Defense"). Reports break out totals by designation; if you do not set it, donations default to the General fund.
  • Soft credits. When a check is written by a spouse, family foundation, or donor-advised fund but the recognition belongs to a different individual, post the donation against the legal donor and set softCreditContactId on the recognition target. Both contacts see the gift in their history.
  • Anonymous gifts. Pass anonymous: true to mark a donation as anonymous for public-facing reports while still attributing it to the contact internally for stewardship and FEC reporting.
  • Recurring versus one-time. If the gift is part of a sustainer schedule, set recurringScheduleId to tie it to the schedule. Reports can then separate recurring revenue from one-time revenue, which most nonprofits and committees need for board updates.

Frequently asked questions

How do I correct a donation amount that was posted incorrectly?
Donations are immutable. Post an offsetting donation (negative amount with notes referencing the original donation ID) and then post a new one with the correct amount. This keeps your audit trail honest and survives FEC and IRS scrutiny.
Can I retrieve a donation by external payment ID?
Not directly. List recent donations for the contact and match on notes for the external ID. For high-volume integrations, store the FundraiserMax donation ID returned from the POST response alongside the external ID in your own translation table to avoid the lookup entirely.
Do I need to send a receipt myself?
No. FundraiserMax sends an IRS-compliant tax receipt automatically when sendReceipt is true. The receipt includes your organization's EIN, contact info, the donation amount, the date, and the boilerplate required by IRS Publication 1771 for charitable contributions.
How do contribution limits get enforced?
For political committees, FundraiserMax tracks each individual's cumulative contributions per election cycle and rejects a donation that would exceed the federal limit ($3,500 per election as of 2026, indexed). For state committees with different limits, configure them in account settings and the API respects the same caps.
What happens if I post a donation for a contact that does not exist?
The API returns 404 with a clear error. Best practice is to create the contact in the same call chain (POST to /v1/contacts first), or to look it up by email before posting the donation. Both approaches are documented in the Contacts API guide.
GET/v1/donations

Retrieve a paginated list of donations. Supports date range filtering for reporting.

NameTypeRequiredDescription
accountIdstringYesYour organization account ID.
limitnumberNoNumber of records to return (default 20, max 100).
offsetnumberNoNumber of records to skip for pagination (default 0).
startDatestringNoISO 8601 date. Only return donations on or after this date.
endDatestringNoISO 8601 date. Only return donations on or before this date.
curl -X GET "https://api.fundraisermax.com/api/v1/donations?accountId=acct_123&startDate=2026-01-01&endDate=2026-02-16&limit=20" \
  -H "X-API-Key: fmx_your_key_id" \
  -H "Authorization: Bearer your_api_secret"

Response

{
  "data": [
    {
      "id": "don_jkl654",
      "contactId": "cnt_abc123",
      "contactName": "Jane Doe",
      "amount": 500.00,
      "donationDate": "2026-01-15T00:00:00Z",
      "paymentMethod": "credit_card",
      "campaignId": "cmp_xyz789",
      "campaignName": "Spring Annual Fund 2026",
      "notes": "Recurring monthly gift",
      "createdAt": "2026-01-15T10:30:00Z"
    }
  ],
  "total": 47,
  "limit": 20,
  "offset": 0
}
POST/v1/donations

Record a new donation. The donation will be associated with the specified contact and optionally linked to a campaign.

NameTypeRequiredDescription
accountIdstringYesYour organization account ID.
contactIdstringYesThe ID of the donor contact.
amountnumberYesDonation amount in USD.
donationDatestringYesISO 8601 date of the donation.
paymentMethodstringNoPayment method: credit_card, check, cash, bank_transfer, other.
campaignIdstringNoOptional campaign to associate with this donation.
notesstringNoFree-text notes about the donation.
curl -X POST "https://api.fundraisermax.com/api/v1/donations" \
  -H "X-API-Key: fmx_your_key_id" \
  -H "Authorization: Bearer your_api_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acct_123",
    "contactId": "cnt_abc123",
    "amount": 250.00,
    "donationDate": "2026-02-16",
    "paymentMethod": "check",
    "notes": "Annual pledge payment 2 of 4"
  }'

Response

{
  "id": "don_mno987",
  "contactId": "cnt_abc123",
  "contactName": "Jane Doe",
  "amount": 250.00,
  "donationDate": "2026-02-16T00:00:00Z",
  "paymentMethod": "check",
  "campaignId": null,
  "campaignName": null,
  "notes": "Annual pledge payment 2 of 4",
  "createdAt": "2026-02-16T15:00:00Z"
}

Frequently asked questions

How do I record a recurring donation?

Each charge in a recurring series is a separate POST to /v1/donations. Use the notes field to tag the series (for example "recurring:monthly:2026") so you can filter or aggregate later. Recurring schedule management itself lives in the Stripe or ActBlue dashboard you process payments through.

Can I refund or void a donation through the API?

Donations are immutable. To represent a refund, POST a second donation with a negative amount and a notes value referencing the original donation ID. Reports and the dashboard treat negative amounts as offsetting credits.

How are donations linked to FEC contribution limits?

When contactId resolves to a contact with occupation, employer, and address fields populated, the donation counts toward that contact's per-cycle contribution limit automatically. See the Campaign Finance feature for the compliance rules applied.

What time zone are dates stored in?

All donationDate, createdAt, and other timestamps are normalized to UTC. Pass dates in ISO 8601 format; if you provide a date-only string ("2026-02-16"), it is interpreted as midnight UTC.

Will an automated thank-you email fire when I POST a donation?

Yes — provided the contact has a deliverable email address and the campaign (if specified) has an active thank-you template. Thank-you sends honor unsubscribe state and the contact's communication preferences.

Related resources