> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Create or Find a Donor

> The workflow for ensuring a donor record exists in Raise before submitting a donation or other donor-dependent operation — covering the search-then-act pattern and the implicit creation path through POST /api/Raise/give.

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

| Scenario                                                                                     | Approach                                                                                                                         |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Submitting a donation where Raise should match or create the donor automatically             | Skip 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 donation | Search 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 operation         | Use the search/lookup pattern below.                                                                                             |
| Updating an existing donor's information                                                     | Find 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](/raise/workflows/process-a-donation#step-2-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:

```bash cURL theme={null}
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 JavaScript theme={null}
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` when                                            | Use `POST /api/Donor/query` when                                  |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
| You have a single identifier (email, phone, ID) and want a quick lookup | You need structured filtering (multiple fields with AND/OR logic) |
| Type-ahead suggestions in a UI                                          | Batch 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:

```bash cURL theme={null}
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](/raise/pagination#discovering-query-options) and the [Spec gap warning](/raise/pagination#the-filter-structure) on operator integer values.

```javascript JavaScript theme={null}
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:

```bash cURL theme={null}
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 JavaScript theme={null}
// 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.

<Note>
  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.
</Note>

### 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 JavaScript theme={null}
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

```bash cURL theme={null}
# 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:

```bash cURL theme={null}
# 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](/raise/concepts/donors#sub-resource-endpoints) for the full sub-resource surface.

***

## A complete create-or-find function

Pulling the patterns together:

```javascript JavaScript theme={null}
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:

| Defense                                         | Description                                                                                                                             |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Use `POST /api/Raise/give` for matching**     | The donation endpoint handles matching atomically — concurrent submissions for the same donor email don't create duplicates.            |
| **Serialize by email at the integration level** | If your integration manages donor creation outside of `/api/Raise/give`, use a per-email lock or queue to serialize concurrent creates. |
| **Detect and merge**                            | If duplicates do occur, use `PUT /api/Donor/merge` to consolidate them — see [Donors: Merge](/raise/concepts/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

<CardGroup cols={2}>
  <Card title="Process a Donation" icon="hand-holding-dollar" href="/raise/workflows/process-a-donation">
    The donation submission workflow that creates donors implicitly through `POST /api/Raise/give`.
  </Card>

  <Card title="Query Donors by Filters" icon="filter" href="/raise/workflows/query-donors-by-filters">
    The structured-query pattern for bulk donor reads.
  </Card>

  <Card title="Donors" icon="user" href="/raise/concepts/donors">
    The Donor resource reference — fields, sub-resources, and special operations.
  </Card>

  <Card title="How Raise Data Flows to CRM+" icon="arrow-right-arrow-left" href="/raise/concepts/data-flow-to-crm">
    How donors created in Raise become Contacts in CRM+.
  </Card>
</CardGroup>
