Developer Documentation

Donations API Reference

Query and record donations with the FundraiserMax Donations API. List donations by date range with pagination, or create one against a contact.

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 feed a dashboard of your own.

The API lists and creates donations; it doesn't update or delete them. Corrections are made in the app, where a user can edit the donation on the contact's record. 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 WinRed (CSV or webhook), from Anedot (webhook), and from any CSV file. 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 after a successful charge in your payment provider. The gift lands on the donor's record and in the campaign's total.
  • 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.
  • Your own 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. Record the acknowledgment. FundraiserMax doesn't email receipts or thank-yous on its own. When your acknowledgment goes out, set thankYouSent: true and thankYouSentDate on the donation (or tick the flag in the app) so the record shows it was done.
Refunds and disputes. Subscribe to Stripe's charge.refunded and charge.dispute.created webhooks. When they fire, find the donation by the charge ID in notes and set its status to refunded or disputed in the app. Amounts must be positive, so a negative offsetting donation isn't an option.

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 total given, last gift date, and year-over-year reporting all anchor on it.

  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.

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 donationAmount as decimal dollars (125.00). A common bug is sending 12500 intending $125.00 and creating a record for $12,500.
  • Where the gift belongs. Every donation needs at least one of campaignId, committeeId, or donationAccountId; the request is rejected without one. Political committees should set committeeId so the gift is attributed to the right committee.
  • Contribution limits are checked, never enforced. A donation posted with a donationAccountId is checked against that donation account's contribution limit (per election, two-year cycle, calendar year, or per donor, set in the app under Financials > Compliance). The API still accepts a donation that goes over, because the money may already be in hand. The app flags it on Financials > Compliance, where the treasurer redesignates the excess, records a refund, or marks it reviewed. Set electionType and electionYear to say which election a gift is for; without them it counts toward the next election after its date, or the account's default election if one is set.
  • On behalf of, and facilitated by. When a check comes from a spouse, family foundation, or donor-advised fund, post the donation against the legal donor and set onBehalfOfContactId to the person it's credited to. Use facilitatedByContactId for the bundler or solicitor who brought it in.
  • Anonymous gifts. Pass isAnonymous: true to mark the gift anonymous on the record. It still counts toward the contact's giving history.
  • Recurring versus one-time. If the gift is one charge in a sustainer series, set isRecurring: true. Each charge is still its own donation record.

Frequently asked questions

How do I correct a donation amount that was posted incorrectly?
Edit it in the app. The API has no update endpoint, so open the donation on the contact's record and change the amount there. Amounts must be positive, so an offsetting negative donation isn't an option.
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?
Yes. FundraiserMax doesn't email receipts or thank-yous when a donation is posted. Send yours, then set thankYouSent: true and thankYouSentDate on the donation so the record shows it went out.
What happens when a donation goes over a contribution limit?
The API accepts it, and the app flags it. Each donation account can have a limit per donor, set under Financials > Compliance. Every donation saved against that account, including ones posted through the API, is added to the donor's total for the period. One that puts the donor over shows up on Financials > Compliance with the amount over and a resolve-by date, for the treasurer to redesignate, refund, or mark reviewed. If you want to hold a gift back before it's recorded, check on your side before posting.
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

{
  "donations": [
    {
      "id": "don_jkl654",
      "contactId": "cnt_abc123",
      "contactName": "Jane Doe",
      "donationAmount": 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 against a contact. Credit it to a campaign, a committee, or a donation account; at least one is required.

NameTypeRequiredDescription
accountIdstringYesYour organization account ID.
contactIdstringYesThe ID of the donor contact.
donationAmountnumberYesDonation amount in USD. Must be greater than zero.
donationDatestringYesISO 8601 date of the donation.
paymentMethodstringYesPayment method: cash, check, credit_card, ach, wire, paypal, venmo, zelle, cryptocurrency, in_kind, other.
campaignIdstringNoCampaign to credit. One of campaignId, committeeId, or donationAccountId is required.
donationAccountIdstringNoDonation account to credit (GET /v1/campaigns/:id lists a campaign's donationAccounts). Contribution limits are checked per donation account.
electionTypestringNoThe election the gift is for: primary, general, runoff, special, convention, recount, or other. Send with electionYear.
electionYearnumberNoYear of that election, e.g. 2026. Without electionType and electionYear the gift counts toward the next election after its date.
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",
    "campaignId": "cmp_xyz789",
    "donationAmount": 250.00,
    "donationDate": "2026-02-16",
    "paymentMethod": "check",
    "notes": "Annual pledge payment 2 of 4"
  }'

Response

{
  "id": "don_mno987",
  "contactId": "cnt_abc123",
  "campaignId": "cmp_xyz789",
  "donationAmount": 250.00,
  "donationDate": "2026-02-16T00:00:00Z",
  "paymentMethod": "check",
  "status": "completed",
  "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") and set isRecurring: true. The schedule itself lives with the payment processor you charge through.

Can I refund or void a donation through the API?

Not through the API, which only lists and creates donations, and amounts must be positive. Open the donation in the app and set its status to refunded.

Does the API check FEC contribution limits?

It checks them but never rejects. A donation posted with a donationAccountId counts toward the donor's total against that account's limit, and one that goes over is flagged in the app on Financials > Compliance for the treasurer to redesignate, refund, or mark reviewed. The response doesn't say whether a donation was flagged, so if you need to stop a gift before it's recorded, check on your side first. See the Campaign Finance feature for what the app checks.

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?

No. FundraiserMax doesn't send thank-you emails when a donation is posted. Send your own, then set thankYouSent and thankYouSentDate on the donation so the record shows it.

Related resources