MailForce API
Overview
The API exposes one resource today: donations. A donation record includes the amount, the payment method, the campaign that prompted it, the donor's name and address, and the contact details we have on file. Each call returns JSON. Filtering by date is the primary access pattern.
If you previously consumed our nightly CSV export, the /donations/caging endpoint returns the same fields with the same column names, so migrating is mostly a matter of switching from a file pull to an HTTP call.
Which donations are returned
The API exposes finalized donation records only. A donation becomes visible once it has been processed by our back-office team and reached one of three terminal states:
| Status | Meaning |
|---|---|
| Submitted | The donation has been reconciled against a deposit and submitted to the bank, but the deposit receipt is not yet on file. |
| Deposited | The donation has been reconciled, submitted, and the bank deposit receipt is on file. |
| Completed | A correspondence-only record — for example, a mail piece received without an enclosed gift. These records do not pass through bank reconciliation; they reach Completed the moment their batch is attached to a deposit. |
Records still in-flight inside MailForce (statuses Open or Reconciled) and records that have been deleted are not returned. This means the same record may appear in a later sync than your first one — for example, a donation reconciled on Monday may only reach Submitted on Tuesday. Use the updated_since filter to pick up records that have changed since your previous run.
To include or isolate correspondence-only entries, use the type filter on the list endpoint: type=donor returns only records with a payment method, type=non_donor returns only correspondence-only records, and omitting the filter returns both.
Quickstart
- Email your MailForce administrator and request API credentials. They will issue you a client ID, a client secret, and one or both of the available scopes.
- Exchange those credentials for a short-lived access token by POSTing to
/oauth/token(see Authentication). - Call any endpoint with the token in an
Authorization: Bearer <token>header. Tryhttps://ign.mailforceapp.com/api/v1/donations/fieldsfirst to confirm everything is wired up — it returns the list of fields your token can see. - Pull data. The most common pattern is a single-day fetch:
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations?date=2026-04-25'
- Tokens expire. When a request returns 401, repeat step 2 to obtain a fresh one. (Tokens currently last 30 days, but rely on the 401 response rather than a hard-coded TTL.)
Authentication
The API uses the OAuth 2.0 Client Credentials flow. There is no user login — your client ID and secret authenticate the calling application directly.
Exchange credentials for a token
POST https://ign.mailforceapp.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id=YOUR_CLIENT_ID &client_secret=YOUR_CLIENT_SECRET &scope=read-donations read-pii
The response contains an access_token. Send it on every API request:
Authorization: Bearer YOUR_ACCESS_TOKEN
Scopes
Scopes determine which fields you receive. We issue two:
| Scope | What it grants |
|---|---|
| read-donations | Read donation, finder, batch, and payment data (no PII) |
| read-pii | Read donor PII: person, address, contacts, metadata, and historical (old_*) values |
If your token only carries read-donations, fields that require read-pii (names, addresses, phone numbers, email) are silently omitted from responses. Asking for one of those fields explicitly via the optional_fields parameter returns a 403 with the offending field names listed.
Rate limits
Each access token gets its own rate-limit bucket. Exceeding a limit returns 429 Too Many Requests with a Retry-After header indicating how long to wait.
| Limiter | Limit | Applies to |
|---|---|---|
| Standard | 60 requests per minute, per token | Applies to every API endpoint. |
| Bulk export | 10 requests per minute and 200 requests per day, per token | Applies on top of the standard limit when calling the /donations/caging endpoint, since each call can return a full day or month of records. |
Pagination
List endpoints return at most 50 records per page by default (maximum 200, configurable via per_page). When more results exist, a cursor is included in the response so you can fetch the next page. There are no offset / page-number parameters — cursor pagination is stable even if records are added or removed while you page through results.
Response shape
Every list response includes a meta block:
{
"data": [ /* ... up to per_page records ... */ ],
"meta": {
"fields_returned": ["id", "finder", "donation_amount", "..."],
"per_page": 50,
"next_cursor": "eyJjcmVhdGVkX2F0Ijoi…",
"prev_cursor": null
}
}
meta.next_cursor— opaque token for the next page.nullwhen you are on the last page (no more results).meta.prev_cursor— token to step back to the previous page.nullon the first page.meta.per_page— echoes the page size in effect for this response.meta.fields_returned— the fields included indata, after applying your scopes and anyoptional_fields.
Fetching the next page
Take the next_cursor value from the previous response and pass it as the cursor query parameter. Do not modify or decode the cursor — treat it as an opaque string. Keep your other filters (date_from, date_to, optional_fields, etc.) on every request; they are not encoded into the cursor.
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations?date_from=2026-04-01&date_to=2026-04-25&cursor=eyJjcmVhdGVkX2F0Ijoi…'
Worked example: pulling every donation in a window
params = { date_from: "2026-04-01", date_to: "2026-04-25", per_page: 200 }
all = []
loop:
response = GET /api/v1/donations?{params}
all.append(response.data)
if response.meta.next_cursor is null:
break
params.cursor = response.meta.next_cursor
return all
Tips
- Increase
per_pageup to 200 for bulk pulls — fewer requests, fewer round trips, friendlier on rate limits. - Going backwards: pass
meta.prev_cursorascursorto step back. Only useful when displaying paged UI; bulk syncs always go forward untilnext_cursorisnull. - Stable ordering: results are ordered by your chosen
date_field(defaultcreated_at) in your chosenorder(defaultasc), withidas a tiebreaker. The cursor encodes this position, so concurrent inserts will appear in subsequent pages without skipping or duplicating already-paged records. Keepdate_fieldandorderconsistent across paginated requests in the same loop — changing them mid-loop will produce a fresh ordering and your cursor will no longer be valid. - For incremental sync, pair pagination with the
updated_sincefilter: pass an ISO-8601 timestamp from your last successful sync, then page through any records that have changed since. - Rate-limit awareness: each page is one request against the standard limit (60/min). The
/donations/cagingendpoint also counts against the bulk-export limit (10/min, 200/day) — plan large pulls accordingly.
Donations
Read donations and the donor information attached to them.
GET/api/v1/donations
Returns a paginated list of donations. Use the date filters to retrieve donations entered on a particular day or within a window.
Parameters
- date
- A single calendar day, formatted YYYY-MM-DD (for example, 2026-04-25). Returns donations entered on that day (or, if you pass `date_field=mail_date`, donations whose campaign was mailed on that day).
- date_from, date_to
- A date range, inclusive. The window cannot exceed 31 days. Subject to the same `date_field` toggle as `date`.
- date_field
- Which column the `date`, `date_from`, and `date_to` filters apply to. `created_at` (default) returns donations by the day they were recorded in MailForce. `mail_date` returns donations by the day the campaign was mailed. Results are also ordered by this column.
- order
- Sort direction for the date column: `asc` (default, oldest first) or `desc` (newest first). The donation's ID is always used as a tiebreaker so cursor pagination remains stable when multiple records share the same date.
- updated_since
- An ISO-8601 timestamp. Returns only donations whose record has changed at or after this time. Useful for incremental sync.
- mail_code
- Filter to donations that responded to a specific campaign code.
- payment_method
- Filter by payment type (cash, check, credit-card, etc.).
- type
- Filter by record type. `donor` returns donations with a payment method (real financial transactions). `non_donor` returns correspondence-only records (e.g., a mail piece received without an enclosed gift). Omit to return both.
- donor_id, finder_id, status_id
- Filter to a specific donor, finder, or workflow status.
- optional_fields
- Comma-separated list of optional-tier field names to include in the response (or "*" for everything your scopes allow). See the Field reference below.
- per_page
- Page size. Default 50, maximum 200.
- cursor
- Opaque pagination cursor returned in the previous response's meta.next_cursor. Omit on the first page.
- format
- Set to "caging" to receive keys in the same column-name format as our reconciliation export. Default is snake_case keys.
Example
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations?date=2026-04-25'
GET/api/v1/donations/caging
Returns the same fields as our internal reconciliation export, keyed by the export's column names. Use this if you are migrating from the nightly file feed and want the response to match its column headers exactly. Requires either a `date` or a `date_from` + `date_to` window. Carries an additional, lower rate limit on top of the standard one.
Parameters
- date or date_from + date_to
- Required. Same semantics as /donations.
- optional_fields, per_page, cursor
- Same as /donations.
Example
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations/caging?date=2026-04-25'
GET/api/v1/donations/{id}
Retrieves a single donation by its unique identifier.
Parameters
- id
- The donation's UUID, returned in the `id` field of every list response.
- optional_fields
- Same as /donations.
- format
- Same as /donations.
Example
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations/9c2d…'
GET/api/v1/donations/fields
Lists every field your token is allowed to receive, generated live from the server. Useful for verifying which optional fields you can request and what the current default response shape is. Returned data matches the Field reference table below.
Example
curl -H "Authorization: Bearer $TOKEN" \ 'https://ign.mailforceapp.com/api/v1/donations/fields'
Field reference
Every field a donation response can include is listed below. The list is generated live — what you see here matches what the API actually returns today.
- Scope — which scope your token must hold to receive this field.
- Tier — default fields are returned automatically; optional fields require
?optional_fields=name1,name2(or?optional_fields=*to include everything your scopes allow). - Caging column — the column header used when calling with
format=caging, matching our internal reconciliation export.
| Field | Scope | Tier | Caging column | Description |
|---|---|---|---|---|
| id | any | default | — | Donation UUID |
| created_at | any | default | — | Donation creation timestamp (ISO-8601) |
| updated_at | any | default | — | Donation last-update timestamp (ISO-8601) |
| finder | read-donations | default | Finder | Finder reference number assigned to the donor |
| mail_code | read-donations | default | Mail_Code | Mail code from finder; defaults to "WM" (white mail) when absent |
| donation_amount | read-donations | default | Donation_Amount | Amount given. Most donations are a single number; gifts split across funds may be returned as an array. |
| mail_date | read-donations | default | Mail_Date | Effective mail date for reporting purposes, formatted m/d/Y. See glossary. |
| payment_type | read-donations | default | Payment_Type | Payment method (cash, check, credit-card, etc.) |
| fulfillment | read-donations | optional | Fulfillment | Donor fulfillment preference |
| quarterly | read-donations | optional | Quarterly | "T" when donor mail notification cadence is quarterly, otherwise empty |
| yearly | read-donations | optional | Yearly | "T" when donor mail notification cadence is annually, otherwise empty |
| biyearly | read-donations | optional | BiYearly | "T" when donor mail notification cadence is biyearly, otherwise empty |
| petition | read-donations | optional | Petition | Donor petition flag |
| planned_giving | read-donations | optional | Planned_Giving | Donor planned giving flag |
| title | read-pii | default | Title | Person title/prefix from metadata |
| first | read-pii | default | First | Person first name |
| middle | read-pii | default | Middle | Person middle name |
| last | read-pii | default | Last | Person last name |
| suffix | read-pii | default | Suffix | Person name suffix from metadata |
| spouse | read-pii | optional | Spouse | Spouse person ID, if linked |
| company | read-pii | default | Company | Company name from metadata |
| street_address | read-pii | default | Street_Address | Street address (default address) |
| city | read-pii | default | City | City (default address) |
| state | read-pii | default | State | State (default address) |
| zip | read-pii | default | Zip | ZIP/postal code (default address) |
| optional_address1 | read-pii | optional | Optional_Address1 | Optional address line 1 (reserved; currently empty in caging report) |
| optional_address2 | read-pii | optional | Optional_Address2 | Optional address line 2 (reserved; currently empty in caging report) |
| home_phone | read-pii | optional | Home_Phone | Home phone contact value |
| business_phone | read-pii | optional | Business_Phone | Business phone contact value |
| employer | read-pii | optional | Employer | Employer (reserved; currently empty in caging report) |
| occupation | read-pii | optional | Occupation | Occupation (reserved; currently empty in caging report) |
| read-pii | optional | E_Mail | Email contact value | |
| deceased | read-pii | default | Deceased | Deceased flag (reserved; currently empty) |
| nomail | read-pii | default | Nomail | No-mail flag (reserved; currently empty) |
| norent | read-pii | default | Norent | No-rent flag (reserved; currently empty) |
| nocall | read-pii | default | NoCall | No-call flag (reserved; currently empty) |
| noemail | read-pii | default | NoEmail | No-email flag (reserved; currently empty) |
| old_title | read-pii | optional | Old_Title | Title before the same-day "Update Person" activity |
| old_first_name | read-pii | optional | Old_First_Name | First name before the same-day "Update Person" activity |
| old_middle_name | read-pii | optional | Old_Middle_Name | Middle name before the same-day "Update Person" activity |
| old_last_name | read-pii | optional | Old_Last_Name | Last name before the same-day "Update Person" activity |
| old_suffix | read-pii | optional | Old_Suffix | Suffix before the same-day "Update Person" activity |
| old_company | read-pii | optional | Old_Company | Company before the same-day "Update Person" activity |
| old_street | read-pii | optional | Old_Street | Street before the same-day "Update Address" activity |
| old_address_line2 | read-pii | optional | Old_Address_Line2 | Address line 2 before the same-day "Update Address" activity |
| old_city | read-pii | optional | Old_City | City before the same-day "Update Address" activity |
| old_state | read-pii | optional | Old_State | State before the same-day "Update Address" activity |
| old_zip | read-pii | optional | Old_Zip | ZIP/postal code before the same-day "Update Address" activity |
| birth_date | read-pii | optional | Birth_Date | Birth date (reserved; currently empty in caging report) |
| spouse_birth_date | read-pii | optional | Spouse_Birth_Date | Spouse birth date (reserved; currently empty in caging report) |
| spouse_email | read-pii | optional | Spouse_Email | Spouse email (reserved; currently empty in caging report) |
| salutation | read-pii | optional | Salutation | Formal salutation (reserved; currently empty in caging report) |
| informal_salutation | read-pii | optional | Informal_Salutation | Informal salutation (reserved; currently empty in caging report) |
| mobile_phone | read-pii | optional | Mobile_Phone | Mobile phone contact value (sourced from contacts.name="phone") |
| fax | read-pii | optional | Fax | Fax (reserved; currently empty in caging report) |
Glossary
Terms used throughout the API and the field descriptions:
- Donor
- The individual or organization that gives a donation. Each donor has a person record (name, contact details) and an address.
- Finder
- A short, unique code printed on a mailing piece (e.g. an envelope or reply card) that lets us match an incoming donation back to the specific donor and campaign that prompted it.
- Mail code
- A campaign code that identifies which mailing the donation responded to. Used for attribution and reporting.
- White mail
- A donation that arrived without a finder code attached — for example, a cheque sent in a plain envelope. These are tagged with the mail code "WM" by default.
- Effective mail date
- The date the donation should be reported against. Usually the date the mailing was sent; in some cases it is adjusted by an internal back-office process so that donations from related mailings group together.
- Caging
- The back-office process of opening, recording, and depositing donations. The /donations/caging endpoint returns the same data points used in our nightly reconciliation export.
- Old_* fields
- When donor or address details are corrected on the day a donation is recorded, the previous values are captured in the activity log. The Old_* fields expose those prior values, scoped to changes made on the same calendar day as the donation. They will be empty when no correction was made.
- Token scope
- A permission attached to your access token. We issue two scopes: read-donations (financial fields) and read-pii (donor personal information). A token can hold one or both.
Errors
The API uses standard HTTP status codes. The response body is always JSON; on errors it includes a message and (where applicable) a list of the specific fields that failed.
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, or expired bearer token. Re-authenticate at /oauth/token. |
| 403 | Your token does not hold a scope required by an explicitly opted-in field. The response lists forbidden_fields. |
| 404 | The donation ID does not exist. |
| 422 | Invalid input — for example, an unknown field name in optional_fields, a date window longer than 31 days, or a malformed date. The response body details what is wrong. |
| 429 | Rate limit exceeded. Wait the number of seconds in the Retry-After header before retrying. |
| 503 | The API has been temporarily disabled by an administrator. Contact your MailForce administrator. |
| 5xx (other) | Something went wrong on our side. Please retry with backoff; if it persists, contact your MailForce administrator. |