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

# Neon CRM Data Mapping Overview

> Comprehensive mapping between Neon CRM objects and WeGive donor platform data structures

# Data Mapping Overview

This page describes what actually syncs between WeGive and Neon CRM, and how.

<Warning>
  Sync direction below is described per object, not per field — see the individual data-mapping pages for the field-level detail. "Bidirectional" object-level sync does **not** mean every field mapping is bidirectional; in fact, none currently are (see the Custom Field Mapping section below).
</Warning>

## Core Object Mappings

| WeGive Object | Neon Object | Real Sync Direction |
| - | - | - |
| Donor (individual) | Individual Account | Both — push real-time, pull once daily |
| Donor (company) | Company Account | Both |
| Transaction | Donation | Both |
| Campaign | Campaign | Both |
| Scheduled Donation | Recurring Donation | **Push only** — no pull-back exists |
| Address | Address | **Dead code — never actually runs.** This is a known issue. |

<Note>
  There is no separate "Payment" object mapping distinct from Donation — a Neon donation carries its own nested `payments` array in the same request, not a separate synced entity.
</Note>

## Neon Object Details

### Individual/Company Account

What's real: first/last name (or company `name`), up to 3 email addresses, email/SMS consent flags, mailing and (for individuals) billing address (inline, no phone/fax fields). See the warning below re: address/phone.

<Warning>
  There is no login-credential creation, "no solicitation" flag control, or origin-tracking beyond a fixed `"WeGive"` origin string that's actually implemented meaningfully — `login.password` is hardcoded to a fixed literal password (`'WeGiveTest1234'`) for every donor across every organization, and `noSolicitation` is always sent as `false`. Neither is configurable per donor. The hardcoded shared password is a known issue (low priority — WeGive currently has no active customer on this integration).
</Warning>

### Donation

What's real: amount (fee-adjusted), date, donor account reference, campaign reference (if any), anonymous flag, donor-covered-fee amount, tribute name, and a nested payment (tender type, card/bank detail, last-4 digits).

<Warning>
  There is no "payment processor information" beyond what's described above, and no fund/purpose object mapping — Neon supports a `fund`/`purpose` field on donations, but this integration never populates it (commented out in the payload-generation code).
</Warning>

### Campaign

What's real: name, start/end date, goal, active/inactive status, generated campaign page and donation form URLs (push); statistics (`donationAmount`, `donationCount`, `eventRegistrationAmount`, `eventRegistrationCount`, `grandTotal`) computed by Neon and read on pull.

<Warning>
  There is no campaign description, campaign hierarchy (parent/child), or fund/purpose association implemented — all exist as commented-out fields in the payload-generation code, not working features.
</Warning>

### Recurring Donation

What's real: amount, next payment date, and a `recurringPeriod`/`recurringPeriodType` pair — push only, create/update, no pull.

<Warning>
  `recurringPeriodType` is always `"LIFE"`. There is no campaign, fund, purpose, or end-date mapping for recurring donations — all commented out in the code. See the dedicated [Recurring Donation Mapping](/external/onboarding/neon/data-mapping/recurring-donation) page for the frequency/period detail.
</Warning>

### Address

<Warning>
  **Dead code.** `syncAddress()`/`generateAddressParams()` are fully implemented but never called anywhere in the codebase — no `pushAddress()` entry point exists on `Neon.php`. Address sync has never actually run for any organization. This is a known issue.
</Warning>

What would be real if this were wired up: address lines, city, zip, and phone numbers — push only, no pull. What's actually real today: a stripped-down inline address copy (no phone) within the donor account push — see [Account Mapping](/external/onboarding/neon/data-mapping/account).

## Default Field Mappings

### Account/Donor Mapping (all fields below are push-only in practice — see the warning further down)

#### Individual Donor → Individual Account

| WeGive Field | Neon Field |
| - | - |
| `first_name` | `primaryContact.firstName` |
| `last_name` | `primaryContact.lastName` |
| `email_1` | `primaryContact.email1` |
| `email_2` | `primaryContact.email2` |
| `email_3` | `primaryContact.email3` |
| `email_notifications` | `consent.email` |
| `sms_notifications` | `consent.sms` |
| `neon_account_id` (WeGive-stored) | `accountId` |
| `neon_id` (WeGive-stored) | `primaryContact.contactId` |

#### Company Donor → Company Account

| WeGive Field | Neon Field |
| - | - |
| `name` | `name` |
| `neon_account_id` (WeGive-stored) | `accountId` |

<Note>
  There is no separate company-specific email/phone field distinct from what's already listed for individual donors — company donor pushes reuse the same `primaryContact` structure (with `firstName` sent as `null`).
</Note>

### Transaction/Donation Mapping

| WeGive Field | Neon Field |
| - | - |
| `amount`, `fee` (n/a for donations — see below) | `amount` |
| `created_at` | `date` |
| `owner` (donor) | `accountId` (indirect — via the donor's own `neon_account_id`) |
| `campaign.neon_id` | `campaign.id` |
| `anonymous` | `anonymousType` |
| `cover_fees` ? `fee_amount` : 0 | `donorCoveredFee` |
| `tribute_name` | `tribute.name` |
| `neon_id` (WeGive-stored) | `id` |
| `neon_payment_id` (WeGive-stored) | `payments[0].id` |

<Note>
  Transaction amount for donations is **not** fee-adjusted the way DonorPerfect's gift-amount is — `generateDonationParams()` sends `amount / 100` directly (the full transaction amount), and `donorCoveredFee` is reported as a separate field alongside it, not subtracted from `amount`.
</Note>

#### Payment Method Mapping

<Warning>
  Only `card`, `bank`, and `donor` are mapped source types, all falling back to Check for anything unmapped. There is **no** distinct Cash, Stock/Securities, In-Kind, PayPal, Wire, or Gift Certificate tender type ever sent by this integration, despite Neon CRM supporting all of these as a platform.
</Warning>

| WeGive Source Type | Neon Tender Type |
| - | - |
| `card` | Credit Card Offline |
| `bank` | Check |
| `donor` | Check |
| anything else | Check (fallback) |

#### Card Type Mapping

| WeGive Card Type | Neon Card Code |
| - | - |
| `visa` | V |
| `mastercard` | M |
| `amex` | A |
| `discover` | D |

<Note>
  Any other card issuer has no code mapped at all — the lookup returns `null`/undefined for `cardTypeCode`.
</Note>

### Campaign Mapping

| WeGive Field | Neon Field | Direction |
| - | - | - |
| `name` | `name` | push |
| `goal` | `goal` | push |
| `start_date` | `startDate` | push |
| `expiration` | `endDate` | push |
| `total_donated` | `statistics.donationAmount` | pull (**not actually imported** — `importCampaigns()` only reads `id`/`name`/`status`; see [Campaign Mapping](/external/onboarding/neon/data-mapping/campaign)) |
| `number_of_donations` | `statistics.donationCount` | pull (not actually imported) |
| `total_registration_collected` | `statistics.eventRegistrationAmount` | pull (not actually imported) |
| `number_of_registrations` | `statistics.eventRegistrationCount` | pull (not actually imported) |
| `neon_id` (WeGive-stored) | `id` | correlation |

<Warning>
  The registration-related statistics fields exist as seeded `NeonMappingRule` defaults, but since Neon's field-application code doesn't consult `NeonMappingRule` on pull at all (see below), and `importCampaigns()`'s own hardcoded fetch pulls `id`/`name`/`status` only — none of the statistics fields are actually populated on pull today, contrary to what the seeded defaults would suggest. This is a known issue.
</Warning>

### Address Mapping

<Warning>
  The table below describes `generateAddressParams()`'s field mapping for reference — but this code path is never invoked (known issue). The address data that actually reaches Neon today is the smaller inline copy on the account push — see [Account Mapping](/external/onboarding/neon/data-mapping/account).
</Warning>

| WeGive Field | Neon Field |
| - | - |
| `address_1` | `addressLine1` |
| `address_2` | `addressLine2` |
| `city` | `city` |
| `state` | `stateProvince.code` |
| `zip` | `zipCode` |
| `primary` | `isPrimaryAddress` |

<Note>
  There's no country-code mapping in this (unreachable) code either — `generateAddressParams()` doesn't populate a `country` field at all.
</Note>

## Custom Field Mapping System

JSONPath-based mapping is real, via `NeonMappingRule`:

```json theme={null}
{
  "integration_path": "primaryContact.addresses[0].addressLine1",
  "wegive_path": "mailingAddress.address_1",
  "crm": "NEON",
  "integration": "ACCOUNT"
}
```

### Supported Objects

`NeonMappingRule::integration` accepts: **ACCOUNT** (1), **DONATION** (2), **CAMPAIGN** (3), **RECURRING\_DONATION** (4), **ADDRESS** (5) — same shared enum used by other CRM integrations' mapping rules (values 6-10 are used by other CRMs, not Neon).

<Warning>
  **Every default and custom `NeonMappingRule` is push-only in practice**, regardless of its `level` value. `generateAccountParams()`/`generateDonationParams()` (both push functions) are the only code that ever reads `NeonMappingRule` — `SyncNeon`'s pull methods (`importDonors()`, `importDonations()`, `importCampaigns()`) use their own separate hardcoded field lists and never consult `NeonMappingRule` at all. A rule configured with `level = 'import'` or `'both'` has no pull-side consumer. This is a known issue.
</Warning>

<Warning>
  The `literal` flag (meant to send a fixed value instead of resolving `wegive_path` as JSONPath) is **not honored** by Neon's field-application code. This is a known issue, with a fix in review.
</Warning>

## Data Synchronization Rules

### Correlation IDs

* `neon_account_id` — Neon account id (donors)
* `neon_id` — Neon contact id (donors), or Neon donation/recurring-donation/address/campaign id depending on the model
* `neon_payment_id` — Neon payment id (transactions only)

### Matching Logic (push)

1. Correlation id (`neon_account_id`/`neon_id`) if already set → update
2. No correlation id → create; on Neon's `10012` (duplicate) error, search by email and link the existing account instead of retrying create
3. There is no name+organization fallback matching — only the email-search-on-duplicate-error path above

### Matching Logic (pull)

1. Email exact match against an existing WeGive `User`
2. No match → create a new `Donor` + `User` + `Login`
3. There is no "last modified wins"/field-level conflict resolution mechanism — pull simply overwrites the mapped fields it fetched; push simply sends whatever WeGive currently has. There's no timestamp comparison deciding which side "wins."

## API Endpoint Reference

### WeGive Dashboard API (real, authenticated)

* `GET /neon-integration` — retrieve settings
* `PUT /neon-integration` — update settings
* `POST /neon-integration/sync` — trigger manual sync
* `POST /neon-mapping-rules` — create/update mapping rules
* `DELETE /neon-mapping-rules/{neon_mapping_rule}` — remove a mapping rule

<Note>
  There is no public `/donors`, `/transactions`, `/campaigns`, `/scheduled-donations` REST API surface specific to this integration — those are general WeGive dashboard/public API endpoints, not integration-specific routes.
</Note>

### Neon CRM API (real, called by this integration)

* `POST /accounts/search`, `POST /accounts`, `PUT /accounts/{id}`, `GET /accounts/{id}`
* `POST /donations/search`, `POST /donations`, `PUT /donations/{id}`
* `GET /campaigns`, `POST /campaigns`, `PUT /campaigns/{id}`
* `POST /recurring`, `PUT /recurring/{id}`
* `POST /addresses`, `PUT /addresses/{id}` — implemented in code (`generateAddressParams()`) but never actually called; this is a known issue
* `POST /webhooks`, `GET /webhooks`, `DELETE /webhooks/{id}`

<Note>
  There is no `GET /accounts` (list-all), `GET /donations/{id}` (single-get outside the `linkDonorFromResponse` helper), or `GET /addresses`/`GET /recurring` used anywhere in this integration's code.
</Note>

## Integration Notes

### Validation

* Neon donor pull requires a non-blank email — a missing/invalid email skips the row entirely
* There is no ISO 8601 enforcement, phone-number normalization, or amount/currency format validation performed by WeGive — values are sent as-is; any rejection surfaces as a Neon API error

### Performance

<Warning>
  There is no rate limiting, batching optimization, or automatic retry in this integration. Every API call is a single unretried `Http::timeout(120)` request.
</Warning>

## Related Documentation

* [Account Mapping](/external/onboarding/neon/data-mapping/account)
* [Donation Mapping](/external/onboarding/neon/data-mapping/donation)
* [Campaign Mapping](/external/onboarding/neon/data-mapping/campaign)
* [Recurring Donation Mapping](/external/onboarding/neon/data-mapping/recurring-donation)
* [Address Mapping](/external/onboarding/neon/data-mapping/address)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.