> ## 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.

# Data Modeling Best Practices

> How to structure donor, gift, and campaign data in Raise — the modeling decisions that determine how clean reporting is, how well integrations behave, and how easy it is to evolve the schema later.

The decisions a customer makes about how to structure their data in Raise — what's a Campaign vs. a Segment, when to create a new Project, how to use Custom Fields, when to set `crmKey` — determine whether the data tells a coherent story years later. Partner integrations are often the first place where these structural decisions show up as friction: a reporting integration that can't find the gift it expects, a sync that produces duplicate donors, an analytics dashboard that can't slice by the dimension the customer wants.

This page covers the modeling patterns that produce clean Raise data and the decisions to make explicitly during integration onboarding rather than discovering later.

The audience is partner integration leads helping a customer think through their Raise setup, and partner engineering teams whose integrations depend on the data shape being predictable.

## The five modeling decisions that matter most

In rough order of impact on integration design:

| Decision                                  | Why it matters                                                                       |
| ----------------------------------------- | ------------------------------------------------------------------------------------ |
| **How donors are matched and identified** | Affects every downstream sync, deduplication, and donor-level reporting              |
| **Campaign / Segment / Form structure**   | Determines whether per-channel attribution is meaningful or messy                    |
| **Project (designation) taxonomy**        | Drives the reporting axis for revenue tracking                                       |
| **Custom field placement and naming**     | Adds dimensions partner integrations can use; gets messy if treated as a junk drawer |
| **CRM key seeding**                       | Whether cross-product reconciliation is easy or hard                                 |

The next sections walk through each.

***

## Decision 1: donor matching

The most consequential modeling decision: how donors are identified and matched across submissions.

### Email is the de facto primary key

Raise's donor-matching algorithm — embedded in `POST /api/Raise/give` and in donor lookups — primarily matches on email. The implications:

* **Donors with the same email collapse into one record.** When a donor gives twice and uses the same email both times, both gifts attach to one Donor.
* **Donors who change emails effectively become new donors.** A donor who gave as `bruce@wayne.example` last year and gives as `bruce.wayne@updated.example` this year will produce two distinct Donor records unless the integration explicitly reconciles them.
* **Donors who use a household email shared with a partner can produce ambiguous matches.** "Bruce and Selina Wayne" giving from a shared email may produce one donor record that represents both — or different records if the form captures both names.

### Practices that handle these cases well

For partner integrations interacting with donor records:

| Practice                                                                                        | Description                                                                                                            |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Use email as the matching key** for new donor submissions, matching the platform's behavior   | Avoids creating fragmented donor records                                                                               |
| **Run periodic deduplication checks** to catch cases where the same person has multiple records | Especially valuable after email migrations or major account changes                                                    |
| **Use `PUT /api/Donor/merge`** for confirmed duplicates                                         | The platform's native merge handles the data fold-down                                                                 |
| **Surface ambiguous matches for human review**                                                  | When two records share characteristics but not email, the integration shouldn't decide automatically                   |
| **Handle the household case explicitly**                                                        | Either treat shared-email gifts as one donor (default) or capture both names in the donor record and decide downstream |

For partner integrations creating new donors via submissions, **don't try to outsmart the matching by uniquifying email addresses with suffixes** — the resulting fragmentation is harder to fix later than the duplicate issue you were trying to avoid.

### Organization vs. individual donors

The `isOrganization` flag distinguishes a corporate or foundation donor from an individual. Set it correctly at submission time:

```javascript JavaScript theme={null}
const donor = {
  isOrganization: !!organization,
  firstName: isOrganization ? null : firstName,
  lastName: isOrganization ? null : lastName,
  organizationName: organization || null,
  email: contactEmail,
  // ...
};
```

Conflating individuals and organizations in the same record (an individual donor whose `organizationName` is set, or an organization whose `firstName` is "Wayne" and `lastName` is "Foundation") produces messy reporting and odd-looking thank-you emails.

***

## Decision 2: Campaign and Segment structure

Campaigns and Segments are how the customer reports on revenue by appeal, channel, and time period. The structure determines whether reports answer the questions the customer actually asks.

### The hierarchy

```text theme={null}
Campaign
  └── Segment
        └── Donation Form
              └── Gift
```

* **Campaign** = the umbrella effort (e.g., "2025 Annual Appeal", "Capital Campaign Phase 2")
* **Segment** = the channel or wave within a campaign (e.g., "Email — Q1 2025", "Direct Mail — March")
* **Form** = the specific donation form (e.g., "Annual Appeal Online Form", "Donor Renewal Form")

A gift attributed to a form inherits the form's segment, which inherits the campaign. So a customer with this hierarchy can report on:

* All gifts to the 2025 Annual Appeal (campaign level)
* All gifts driven by email in Q1 2025 (segment level)
* All gifts from a specific form (form level)

### Practices for designing the hierarchy

| Practice                                                           | Description                                                                                                                                                      |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Time-bound campaigns**                                           | Most campaigns have a start and end date. A "2025 Annual Appeal" closes when 2025 ends; "2026 Annual Appeal" begins anew.                                        |
| **Channel-based segments**                                         | Segments cleanly map to channels — email, direct mail, social, paid. This makes "channel performance" reports straightforward.                                   |
| **Sub-channel segments** for major channels                        | If email is a primary channel, sub-divide by audience or appeal — "Email — Lapsed Donors Q1", "Email — Major Donors Q1".                                         |
| **Form per segment** is acceptable when forms are channel-specific | A form embedded in an email blast can be a one-off segment; a general form serving multiple channels typically has one segment per channel using URL parameters. |
| **Resist creating campaigns for everything**                       | Campaigns are heavy; not every appeal needs its own. Use Segments for sub-divisions within an ongoing campaign.                                                  |

### Anti-pattern: segments as Projects

A common mistake: using Segments as a substitute for Projects (designations). Segments are about *how* the donor came in; Projects are about *what* the donor funded. Don't conflate them — a gift to the "General Fund" Project that arrived through the "Email Q1" Segment can answer both "how much did email drive?" and "how much went to general operations?" Separately, you get clean intersections.

### Working with the structure in partner integrations

When reading gifts for analytics, the attribution fields on `GiftModel` provide direct access:

```javascript JavaScript theme={null}
function attributeGift(gift) {
  return {
    campaign: gift.campaignName,
    segment: gift.segment,
    segmentName: gift.segmentName,
    form: gift.form?.title,
    formId: gift.form?.id,
    project: gift.projects?.[0]?.projectName,
  };
}
```

Group, filter, and aggregate by these fields. The customer's reporting questions (revenue by campaign, by segment, by form) become straightforward aggregations.

***

## Decision 3: Project taxonomy

Projects represent the funding destinations gifts designate to. The taxonomy — what Projects exist, how granular they are, how they evolve over time — determines what reports the customer can produce.

### Two opposing pressures

| Pressure                                             | Symptom of going too far                                                                       |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **Granularity** (more Projects = finer reporting)    | Hundreds of Projects, none with meaningful giving, hard to maintain                            |
| **Aggregation** (fewer Projects = simpler reporting) | One "General Fund" Project hides all the program-level revenue distinctions the customer needs |

The right answer is in between, and customer-specific.

### Practices for Project taxonomy

| Practice                                                 | Description                                                                                         |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Start with the customer's existing program structure** | Their accounting team or development office typically has a working program list. Map it 1:1 first. |
| **Aggregate at the right level**                         | If the customer reports on five programs internally, have five Projects (not 50 sub-programs).      |
| **Use one Project per restricted-giving destination**    | Restricted gifts need their own Projects for accounting compliance.                                 |
| **Keep "General Fund" or equivalent as a Project**       | The default destination for unrestricted gifts.                                                     |
| **Don't reuse Project IDs across reorganizations**       | When a program closes, archive the Project (don't repurpose its ID for a new program).              |

### Project codes vs. names

Each Project has both a name (display) and a code (stable identifier used in `projectOverrideCode` URL parameters and similar). The code should:

* Be short and code-like (`GOTHAM-OUTREACH`, not `Gotham Outreach Program 2025`).
* Be stable across the Project's lifetime — don't rename it as the program evolves.
* Be unique across the organization.

If the code changes, every URL parameter using `projectOverrideCode=OLD-CODE` breaks. Treat codes as part of the integration's public contract.

### Working with Projects in partner integrations

For analytics integrations, the project allocation is in `gift.projects[]`:

```javascript JavaScript theme={null}
function revenueByProject(gifts) {
  const totals = new Map();

  for (const gift of gifts) {
    for (const allocation of gift.projects ?? []) {
      const current = totals.get(allocation.projectName) ?? 0;
      totals.set(allocation.projectName, current + allocation.amount);
    }
  }

  return Object.fromEntries(totals);
}
```

A gift split across multiple Projects contributes to each — the `projects[]` array on a $100 gift split 60/40 has two entries that sum to $100.

***

## Decision 4: Custom Fields

Custom Fields capture data that doesn't fit the standard schema — donor preferences, gift-time questions, organizational metadata. Used well, they add dimensions to reporting and integration logic. Used poorly, they become a junk drawer of inconsistent values.

### Practices for Custom Field design

| Practice                                                                       | Description                                                                                                                                    |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Treat Custom Fields like database schemas**                                  | Each field has a clear name, type, expected values, and purpose. Document these before creating fields.                                        |
| **Use Custom Fields on the right resource**                                    | A field about the donor (preferred communication method) belongs on the Donor; a field about the gift (event ticket type) belongs on the Gift. |
| **Avoid free-text where an enum would work**                                   | A "Source" custom field with values like "Google Ads", "google ads", "Google ads", "Google" produces messy reports. Use a constrained list.    |
| **Don't create Custom Fields for fields the standard schema already supports** | Don't add a "Donor Phone" Custom Field — use the standard `phone` field and `donorPhoneNumbers[]` sub-resource.                                |
| **Plan for evolution**                                                         | Add a "deprecated" prefix or naming pattern for fields you're phasing out so integrations know to ignore them.                                 |

### Working with Custom Fields in partner integrations

For reading Custom Field values on a Donor:

```bash cURL theme={null}
curl https://prod-api.raisedonors.com/api/Donor/12345/donor-fields \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

For Gifts:

```bash cURL theme={null}
curl https://prod-api.raisedonors.com/api/Gift/9876/gift-fields \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

These return the custom field responses captured on the resource. Partner integrations that need to act on Custom Field values (e.g., segmenting donors by a Custom Field response, routing gifts based on event ticket type) pull these alongside the main resource.

### Don't use Custom Fields for everything

A common anti-pattern: treating Custom Fields as the universal extensibility mechanism for every integration's needs. A partner integration that creates 50 Custom Fields on every customer's account pollutes the customer's data model — the customer sees these in their admin UI and has no idea what they're for.

For partner-internal state (sync metadata, processing flags), keep the data in your own database. Use Custom Fields only when:

* The data is meaningful to the customer's reporting or staff workflow.
* The customer would benefit from seeing the field in the Raise admin UI.
* The data belongs to the donor or gift, not to the integration.

***

## Decision 5: CRM key seeding

For customers running Raise alongside CRM+ (or another external CRM), the `crmKey`, `crmSecondKey`, and `crmKeyUrls` fields link Raise records to their counterparts in the external system. See [How Raise Data Flows to CRM+](/raise/concepts/data-flow-to-crm).

The decision: when (if ever) should the partner integration seed these values explicitly versus letting the platform sync populate them?

### When to seed `crmKey` explicitly

| Scenario                                                                                                | Approach                                                     |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Partner is creating a Raise record from data that originated in CRM+                                    | Seed `crmKey` with the CRM+ Contact ID at creation time      |
| Partner is creating a Raise record matched to an external CRM identifier the customer has in their hand | Seed `crmKey` with that identifier                           |
| Partner is creating a Raise record with no external counterpart yet                                     | Don't seed; let the platform sync populate it after the fact |

### When not to seed

| Scenario                                                   | Why                                                        |
| ---------------------------------------------------------- | ---------------------------------------------------------- |
| The partner doesn't know the corresponding CRM+ Contact ID | Don't guess; let the sync find or create the right Contact |
| The customer's CRM+ account is new or empty                | Sync will populate `crmKey` once the Contact is created    |
| The partner integration is read-only against Raise         | No need to write linkage data                              |

### Reading vs. writing `crmKey`

Reading `crmKey` is always useful — it tells the partner integration whether the record has been synced and which external Contact it links to. Writing `crmKey` only matters when the partner has authoritative knowledge of the linkage.

```javascript JavaScript theme={null}
// Reading — always useful
function buildCrmLink(donor) {
  const entries = Object.values(donor.crmKeyUrls ?? {});
  return entries[0]?.url ?? null;
}

// Writing — only when authoritative
async function createRaiseFromCrmContact(crmContact, donationDetails) {
  return fetch('https://prod-api.raisedonors.com/api/Raise/give', {
    method: 'POST',
    headers: { /* ... */ },
    body: JSON.stringify({
      ...donationDetails,
      donor: {
        firstName: crmContact.firstName,
        lastName: crmContact.lastName,
        email: crmContact.email,
        crmKey: crmContact.id.toString(),
      },
    }),
  }).then((r) => r.json());
}
```

***

## Test mode and production separation

Throughout the data model, `isTestMode` flags appear on submissions, donors, gifts, and recurring gifts. Treat test mode as a first-class data axis:

| Practice                                                 | Description                                                                                                                            |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Filter production reports to `isTestMode: false`**     | Test data should never appear in donor counts, revenue totals, or campaign performance                                                 |
| **Tag test-mode usage clearly**                          | Use distinct donor email domains (`test@example.com`, `dev+timestamp@example.com`) so test data is easy to identify                    |
| **Run dedicated test-mode donations during development** | Don't develop against production data; use the [test-payment-method generator](/raise/base-urls#use-the-test-payment-method-generator) |
| **Clean up test-mode data periodically**                 | The customer's admin team can archive or delete accumulated test records                                                               |

Partner integrations should also filter `isTestMode: true` records out of downstream syncs unless explicitly running in a development environment. See [Sync Raise Gifts to an External System: Test-mode in production](/raise/recipes/sync-raise-gifts-to-an-external-system#test-mode-in-production).

***

## Modeling pitfalls to avoid

A few specific anti-patterns that produce messy data:

### Pitfall 1: one Project for everything

Every gift designating to "General Fund" produces a single revenue line in reports. The customer can't slice by program, can't show donors what their gift funded specifically, can't distinguish unrestricted from restricted giving. Add at minimum 5–10 Projects to cover the major programs.

### Pitfall 2: a Campaign per appeal

The opposite problem: every email blast is a new Campaign. Hundreds of Campaigns accumulate, most with a few gifts each, and the "all campaigns" view becomes unreadable. Use Segments for sub-divisions; reserve Campaigns for the major efforts.

### Pitfall 3: Custom Fields as a junk drawer

A customer accumulates 50+ Custom Fields over the years, most with inconsistent values, half of them used by integrations that no longer exist. Periodically audit Custom Fields and archive the ones that aren't actively used.

### Pitfall 4: not setting `isOrganization`

A foundation giving \$50,000 with `isOrganization: false` and `firstName: "Wayne"` `lastName: "Foundation"` looks weird in reports and breaks any logic that filters individual vs. organizational donors. Always set the flag correctly.

### Pitfall 5: using free-text where a Project would do

"What did the donor designate to?" captured in a free-text Custom Field rather than as a Project allocation produces unstructured data. Use Projects for designation; Custom Fields for the things that genuinely don't fit standard fields.

### Pitfall 6: keeping test-mode donors in production reports

A common reporting issue: the customer's "all donors" count includes hundreds of test-mode records from development work. Filter `isTestMode: false` everywhere production-facing.

***

## Onboarding a new customer

For partner integrations onboarding a new customer, walk through these decisions together:

<Steps>
  <Step title="Audit the customer's existing data shape">
    How many Campaigns? Segments? Projects? Are the conventions consistent? Are there orphaned Custom Fields?
  </Step>

  <Step title="Document the canonical taxonomy">
    Write down what Campaigns and Projects exist, what they mean, and what new ones should be added. This becomes the integration's contract.
  </Step>

  <Step title="Set the integration's expectations">
    "Our integration filters out test-mode gifts." "Our integration assumes Projects have stable codes." Make implicit expectations explicit.
  </Step>

  <Step title="Plan for Custom Field needs">
    If the integration needs Custom Fields, propose them with clear names and types — and confirm the customer wants them in their data model.
  </Step>

  <Step title="Coordinate on `crmKey` policy">
    If the customer also runs CRM+, agree on who's responsible for what — the partner integration may or may not need to seed linkage data.
  </Step>

  <Step title="Document and revisit annually">
    The data model evolves. Revisit the taxonomy decisions at least once a year to catch drift.
  </Step>
</Steps>

The investment upfront makes every subsequent integration interaction smoother. A customer with a clean data model produces clean reports, clean syncs, and clean handoffs between systems.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="API Performance Tips" icon="gauge" href="/raise/best-practices/api-performance-tips">
    The performance practices that make integrations against this data model efficient.
  </Card>

  <Card title="Sync Architecture Patterns" icon="diagram-project" href="/raise/best-practices/sync-architecture-patterns">
    How to design integrations that move data between Raise and external systems.
  </Card>

  <Card title="How Raise Data Flows to CRM+" icon="arrow-right-arrow-left" href="/raise/concepts/data-flow-to-crm">
    The cross-product implications of the modeling decisions on this page.
  </Card>

  <Card title="The Raise Data Model" icon="diagram-project" href="/raise/concepts/data-model">
    The resource-level reference for the data the modeling decisions apply to.
  </Card>
</CardGroup>
