Skip to main content
Most workflows that touch a donor begin with the same question: does this donor already exist in Raise, or do I need to create a new record? This page covers the patterns for answering it and the operations that follow. The Raise API doesn’t expose a top-level POST /api/Donor for net-new donor creation. Donor records typically come into existence in one of two ways:
  • Implicitly through POST /api/Raise/give when a donation is submitted (most common).
  • Through PUT /api/Donor/{id} with the full donor record — but this requires an existing donor ID, so it’s effectively an update or upsert at a known ID.
This page documents the search-and-match patterns plus the practical paths for creating donors when explicit pre-creation is needed.

When to use this workflow

ScenarioApproach
Submitting a donation where Raise should match or create the donor automaticallySkip this workflow — let POST /api/Raise/give handle donor matching via the embedded donor block.
Syncing donors from another system (HR, ESP, etc.) into Raise without an associated donationSearch first, then submit a donation in test mode (and refund) to provoke donor creation — or coordinate through the admin team.
Building an integration that needs to verify a donor exists before another operationUse the search/lookup pattern below.
Updating an existing donor’s informationFind the donor first, then call PUT or PATCH against /api/Donor/{id}.
For routine donations, donor matching is handled automatically by the donation submission endpoint — see Process a Donation: Prepare the donor block. This workflow is for the cases where matching needs to happen explicitly.

Pattern 1: lookup by email via GET /api/Donor/search

The cheapest and most direct lookup. The search endpoint accepts a free-text filter string and matches against multiple fields — including ID, name, email, phone, postal code, and state:
cURL
curl "https://prod-api.raisedonors.com/api/Donor/search?filter=bruce@wayne.example" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
The spec documents the supported match patterns explicitly:
Search for donors by a filter string. Can match on donor ID, first name, last name, organization name, phone number, email address, postal code, and state. Supports searching by first/last name combinations, name with state, name with postal code, phone number in (xxx) xxx-xxxx format, and email in angle brackets.
So the search endpoint will accept:
  • A bare email: bruce@wayne.example
  • An email in angle brackets: <bruce@wayne.example>
  • A phone number in (xxx) xxx-xxxx format
  • A name with state qualifier: Bruce Wayne, NJ
  • A name with postal code: Bruce Wayne 10001
  • A bare donor ID
Use bracket-wrapped emails when the search term might otherwise be ambiguous (e.g., when the email’s local part collides with a name).
JavaScript
async function findDonorByEmail(email) {
  const params = new URLSearchParams({ filter: `<${email}>` });
  const response = await fetch(
    `https://prod-api.raisedonors.com/api/Donor/search?${params}`,
    { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
  );

  if (!response.ok) {
    throw new Error(`Search failed: ${response.status}`);
  }

  const results = await response.json();
  // The search endpoint returns matching donors. Take the first exact email match.
  return results.find((d) => d.email?.toLowerCase() === email.toLowerCase()) ?? null;
}

When to prefer search over query

Use /api/Donor/search whenUse POST /api/Donor/query when
You have a single identifier (email, phone, ID) and want a quick lookupYou need structured filtering (multiple fields with AND/OR logic)
Type-ahead suggestions in a UIBatch reconciliation across thousands of donors
Investigating “is this donor in the system?”Building segments or exports
For most integration workflows that ask “does this email already exist as a donor?”, search is the right answer.

Pattern 2: structured filter via POST /api/Donor/query

For more deterministic matching — exact email equality without partial matches on other fields — the query endpoint is more precise:
cURL
curl -X POST https://prod-api.raisedonors.com/api/Donor/query \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skip": 0,
    "take": 1,
    "groups": [
      {
        "conditions": [
          { "parameter": "email", "operator": 0, "value": "bruce@wayne.example" }
        ]
      }
    ]
  }'
The operator: 0 value here is a placeholder — the integer-to-label mapping for query operators must be discovered through GET /api/Query/options/{queryType} since the spec doesn’t expose it. See Pagination and Filtering: Discovering query options and the Spec gap warning on operator integer values.
JavaScript
async function findDonorByEmailStructured(email, equalsOperator) {
  const response = await fetch(
    'https://prod-api.raisedonors.com/api/Donor/query',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        skip: 0,
        take: 1,
        groups: [
          {
            conditions: [
              { parameter: 'email', operator: equalsOperator, value: email },
            ],
          },
        ],
      }),
    }
  );

  const page = await response.json();
  return page.items[0] ?? null;
}
This pattern is more verbose than search but produces exact matches without the fuzzy-matching behavior of the search endpoint. Use it when you need to be certain that the returned donor matches the input email exactly.

Pattern 3: lookup by stored ID via GET /api/Donor/{donorId}

If your integration already has a Raise donor ID stored from a previous operation — typically because a prior gift or interaction surfaced it — the cheapest lookup is by ID directly:
cURL
curl https://prod-api.raisedonors.com/api/Donor/12345 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
A 404 response means the donor was deleted (or the ID is wrong). A 200 returns the full record including all sub-resources when includeDetails semantics apply. This is the cheapest lookup because it’s a single record fetch with no filter evaluation. Cache donor IDs from previous interactions and use them when possible.

When no match is found

If search or query returns no matching donor, three paths forward:

Path A: let POST /api/Raise/give create the donor (recommended for live donations)

If the workflow is heading toward a donation anyway, don’t pre-create the donor. Let the donation submission handle creation:
JavaScript
// Skip the explicit donor pre-creation step
const gift = await fetch(
  'https://prod-api.raisedonors.com/api/Raise/give',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: 50.00,
      paymentMethodId: 'tok_abc123',
      paymentMethodType: 'CreditCard',
      donor: {
        firstName: 'Bruce',
        lastName: 'Wayne',
        email: 'bruce@wayne.example',
      },
    }),
  }
);
Raise will match the embedded donor block by email — creating a new Donor if no match exists, attaching to an existing one if a match is found. The Gift returned in the response carries the donorId of the matched-or-created donor. This is the recommended path for any workflow that will produce a donation. It avoids race conditions where two workflows simultaneously try to create the same donor.

Path B: seed the donor through admin tools (for non-donation onboarding)

For integrations that need donors to exist in Raise without a corresponding donation — for example, a CRM-driven workflow that registers a prospective donor before they’ve given — the practical path is to coordinate with the customer’s admin team:
  • The customer’s Raise administrator can create the donor through the admin UI.
  • The customer’s bulk import tools can ingest a list of donors from a CSV.
The API does not currently expose a direct “create a donor without a gift” endpoint. This is a known constraint of the current API surface.
Future versions of the Raise API are expected to add direct donor-creation endpoints. Until then, partner integrations that need bulk donor seeding should coordinate with the customer’s admin team rather than building around the absence of the endpoint.⚠️ Spec gap: No top-level POST /api/Donor endpoint exists for net-new donor creation. The CreateDonorRequest schema in the spec is referenced only by PUT /api/Donor/{id} (where the donor must already exist). Confirm with the platform team if a true create endpoint is on the v2 roadmap.

Path C: provoke creation through a test-mode donation (development only)

For development environments where you need to script donor creation without an admin UI, you can submit a small test-mode donation through POST /api/Raise/give with isTestMode: true. The donor will be created (or matched) by the donation flow:
JavaScript
async function ensureTestDonor({ firstName, lastName, email }) {
  // Generate a test payment method
  const tokenResponse = await fetch(
    'https://prod-api.raisedonors.com/api/Raise/generate-test-payment-method',
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` },
    }
  );
  const { paymentMethodId } = await tokenResponse.json();

  // Submit a $1 test donation to create or match the donor
  const giftResponse = await fetch(
    'https://prod-api.raisedonors.com/api/Raise/give',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        amount: 1.00,
        paymentMethodId,
        paymentMethodType: 'CreditCard',
        isTestMode: true,
        donor: { firstName, lastName, email },
      }),
    }
  );

  const gift = await giftResponse.json();
  return gift.donorId;
}
Don’t use this path in production. It produces real Gift records (even if test-mode) that need to be filtered out of reporting. It’s an acceptable pattern for development scripts that need to seed donor data, not for production workflows.

Updating a donor after finding them

Once you have a donor ID — from a search match or a freshly-created donor — the update operations are:

Full or partial update of the donor record

cURL
# PATCH for partial updates (recommended for most cases)
curl -X PATCH https://prod-api.raisedonors.com/api/Donor/12345 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "bruce.wayne@updated.example",
    "phone": "(555) 555-0200"
  }'
PATCH /api/Donor/{id} updates only the fields included in the body. PUT /api/Donor/{id} replaces the full record. Prefer PATCH unless you have a specific reason to replace everything.

Adding sub-resources

For adding addresses, emails, or phone numbers without replacing the existing ones, use the sub-resource endpoints:
cURL
# Add an address
curl -X POST https://prod-api.raisedonors.com/api/Donor/12345/address \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "address1": "1007 Mountain Drive", "city": "Gotham", "state": "NJ", "postalCode": "10001" }'

# Add an email contact method
curl -X POST https://prod-api.raisedonors.com/api/Donor/12345/email \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "value": "alt@wayne.example" }'
These add to the donor’s collections without touching existing entries. See Donors: Sub-resource endpoints for the full sub-resource surface.

A complete create-or-find function

Pulling the patterns together:
JavaScript
async function findOrEnsureDonor({ firstName, lastName, email, phone }) {
  // 1. Try to find by email first
  const params = new URLSearchParams({ filter: `<${email}>` });
  const searchResponse = await fetch(
    `https://prod-api.raisedonors.com/api/Donor/search?${params}`,
    { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
  );

  if (searchResponse.ok) {
    const results = await searchResponse.json();
    const match = results.find((d) => d.email?.toLowerCase() === email.toLowerCase());
    if (match) {
      return { donor: match, created: false };
    }
  }

  // 2. No match found. For a live-donation workflow, return null and let
  //    POST /api/Raise/give handle the creation as part of the donation.
  //    For pre-donation workflows, you'd need to coordinate with the admin team.
  return { donor: null, created: false };
}
The function never tries to create a donor through a non-existent API path — it just answers “do we have this donor?” and lets the caller decide what to do next.

A common pitfall: race conditions

Two workflows running in parallel both look up the same donor by email, both find no match, both create the donor. Without explicit locking or idempotency, this can produce duplicate donor records. The defenses:
DefenseDescription
Use POST /api/Raise/give for matchingThe donation endpoint handles matching atomically — concurrent submissions for the same donor email don’t create duplicates.
Serialize by email at the integration levelIf your integration manages donor creation outside of /api/Raise/give, use a per-email lock or queue to serialize concurrent creates.
Detect and mergeIf duplicates do occur, use PUT /api/Donor/merge to consolidate them — see Donors: Merge.
The race-condition defense is one of the strongest arguments for letting POST /api/Raise/give handle donor creation rather than pre-creating donors yourself.

Where to go next

Process a Donation

The donation submission workflow that creates donors implicitly through POST /api/Raise/give.

Query Donors by Filters

The structured-query pattern for bulk donor reads.

Donors

The Donor resource reference — fields, sub-resources, and special operations.

How Raise Data Flows to CRM+

How donors created in Raise become Contacts in CRM+.
Last modified on May 20, 2026