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.
Common use cases
- Record contributions from a custom donation page. POST to
/v1/donationsafter 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/donationswithstartDateset 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. Setnotesto 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
donationDate to the date the donor authorized the charge, not the date your script runs. Reports and FEC filings rely on the authorization date.notes field (for example "stripe:ch_3OqXXXX") and check before re-posting if your import job retries.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.
- 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. - Confirm the charge. Let Stripe (or your processor of choice) handle card collection client-side and confirm the PaymentIntent. Wait for the
payment_intent.succeededwebhook rather than the client-side success callback so a closed browser tab does not cost you a donation record. - Record the donation. POST to
/v1/donationswith the contact ID, the dollar amount,donationDateset to the Stripe charge timestamp, and the Stripe charge ID embedded innotes(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 innotes. - Record the acknowledgment. FundraiserMax doesn't email receipts or thank-yous on its own. When your acknowledgment goes out, set
thankYouSent: trueandthankYouSentDateon the donation (or tick the flag in the app) so the record shows it was done.
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.
- 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
contactIdfrom the table. - Use the original gift date. Set
donationDateto 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 innotesfor transparency. - 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. - 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.
- Pull yesterday's donations. GET
/v1/donationswithstartDateset to yesterday at 00:00 UTC andendDateat 23:59:59. Page until the response returns fewer rows than the limit. - 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.
- 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.
- 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
donationAmountas decimal dollars (125.00). A common bug is sending12500intending $125.00 and creating a record for $12,500. - Where the gift belongs. Every donation needs at least one of
campaignId,committeeId, ordonationAccountId; the request is rejected without one. Political committees should setcommitteeIdso the gift is attributed to the right committee. - Contribution limits are checked, never enforced. A donation posted with a
donationAccountIdis 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. SetelectionTypeandelectionYearto 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
onBehalfOfContactIdto the person it's credited to. UsefacilitatedByContactIdfor the bundler or solicitor who brought it in. - Anonymous gifts. Pass
isAnonymous: trueto 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
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.
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.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.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.
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.Retrieve a paginated list of donations. Supports date range filtering for reporting.
| Name | Type | Required | Description |
|---|---|---|---|
accountId | string | Yes | Your organization account ID. |
limit | number | No | Number of records to return (default 20, max 100). |
offset | number | No | Number of records to skip for pagination (default 0). |
startDate | string | No | ISO 8601 date. Only return donations on or after this date. |
endDate | string | No | ISO 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
}Record a new donation against a contact. Credit it to a campaign, a committee, or a donation account; at least one is required.
| Name | Type | Required | Description |
|---|---|---|---|
accountId | string | Yes | Your organization account ID. |
contactId | string | Yes | The ID of the donor contact. |
donationAmount | number | Yes | Donation amount in USD. Must be greater than zero. |
donationDate | string | Yes | ISO 8601 date of the donation. |
paymentMethod | string | Yes | Payment method: cash, check, credit_card, ach, wire, paypal, venmo, zelle, cryptocurrency, in_kind, other. |
campaignId | string | No | Campaign to credit. One of campaignId, committeeId, or donationAccountId is required. |
donationAccountId | string | No | Donation account to credit (GET /v1/campaigns/:id lists a campaign's donationAccounts). Contribution limits are checked per donation account. |
electionType | string | No | The election the gift is for: primary, general, runoff, special, convention, recount, or other. Send with electionYear. |
electionYear | number | No | Year of that election, e.g. 2026. Without electionType and electionYear the gift counts toward the next election after its date. |
notes | string | No | Free-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.