> ## 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 Donation Mapping

> Detailed mapping between Neon CRM donation objects and WeGive transaction records

# Donation Object Mapping

This document details the real mapping used by `NeonIntegration::generateDonationParams()`.

<Warning>
  **The payment block is only sent on donation CREATE, never on UPDATE.** `generateDonationParams()`'s payments array is gated by `$transaction->neon_payment_id ? null : [...]` — if a payment id is already stored (meaning this transaction was previously created in Neon), the entire `payments` key is sent as `null` on every subsequent update call. Updating a donation's tribute, fee, or campaign never re-sends or corrects the associated payment/tender data.
</Warning>

## Core Donation Mapping

### Financial Information

| WeGive Field | Neon Field |
| - | - |
| `amount` (cents) | `amount` (÷100) |
| `cover_fees` ? `fee_amount` : 0 (cents) | `donorCoveredFee` (÷100) |

<Note>
  There is no `currency` field sent at all — no USD default, no currency key in the payload whatsoever.
</Note>

### Timing Information

| WeGive Field | Neon Field |
| - | - |
| `created_at` | `date` |
| `payout.paid_at` (formatted `Y-m-d`, only if a payout exists) | `receivedDate`\* |

<Note>
  `receivedDate` is not actually a documented top-level key in the params array — the code computes `$renderedData['payout']['paid_at']` on the rendered webhook resource, which only feeds into the custom-field-mapping resolution step (see below), not a hardcoded top-level field. It only reaches Neon if a `NeonMappingRule` maps `payout.paid_at` to some Neon path.
</Note>

### Attribution

| WeGive Field | Neon Field |
| - | - |
| `owner.neon_account_id` | `accountId` |
| `owner.name` | `donorName` |
| `campaign.neon_id` (only if campaign exists **and** already has a `neon_id`) | `campaign.id`, `campaign.name`, `campaign.status = "ACTIVE"` |
| `anonymous` | `anonymousType` |

<Warning>
  `anonymousType` sends the **raw WeGive boolean value directly** — not a translated `"ANONYMOUS"`/`"NOT_ANONYMOUS"` string as a prior version of this page and its JSON examples implied. Whether Neon's API correctly interprets a raw boolean where it likely expects a string enum isn't something WeGive verifies.
</Warning>

<Warning>
  If a transaction's campaign hasn't itself synced to Neon yet (no `neon_id` on the `Campaign` record), the entire `campaign` key is sent as `null` — there's no "sync campaign first" orchestration inside `generateDonationParams()` itself.
</Warning>

### Tribute Information

| WeGive Field | Neon Field |
| - | - |
| `tribute_name` | `tribute.name` |
| — | `tribute.type` — **always** hardcoded `"Honor"` |

<Note>
  `tribute_type` is not a real WeGive field consulted anywhere in this mapping — the value is hardcoded regardless of what kind of tribute (memorial vs. honor) the donation actually represents.
</Note>

### Integration Fields

| WeGive Field (stored) | Neon Field |
| - | - |
| `neon_id` | `id` |
| `neon_payment_id` | Gates whether `payments` is sent at all (see warning above) — not itself sent as a value |

## Payment Processing

### Tender Type Mapping

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

<Warning>
  There is no distinct Cash, Stock/Securities, In-Kind, PayPal, Wire, or Gift Certificate tender type ever sent — only Credit Card Offline and Check, ever, regardless of what Neon's platform itself supports.
</Warning>

### Credit Card Processing (`source_type = 'card'`, create only)

| WeGive Field | Neon Field |
| - | - |
| `source.last_four` (zero-padded to 4 chars) | `creditCardOffline.cardNumberLastFour` |
| `source.issuer` (mapped: visa→V, mastercard→M, amex→A, discover→D; anything else → unmapped/`null`) | `creditCardOffline.cardTypeCode` |
| `source.expiration` (split on `/`) | `creditCardOffline.expirationMonth`/`expirationYear` |
| `owner.email_1` | `creditCardOffline.cardHolderEmail` |
| `owner.name` | `creditCardOffline.cardHolderName` |

<Note>
  `creditCardOffline.billingAddress` is hardcoded `null` — no billing address is ever included on the payment object itself (a stripped copy exists separately, on the donor account push — see [Account Mapping](/external/onboarding/neon/data-mapping/account)).
</Note>

### Bank Transfer Processing (`source_type = 'bank'`, create only)

| WeGive Field | Neon Field |
| - | - |
| `source.last_four` (zero-padded) | `check.accountNumber` |
| `source.name` | `check.institution` |
| `owner.name` | `check.accountOwner` |
| — | `check.accountType` — always hardcoded `"Checking"` |

<Warning>
  If `source` is `null` for any reason, `getLastFour()` returns the literal string `'0000'` rather than throwing — meaning a transaction with no attached payment source would push a donation with a fabricated-looking last-4 of `0000` rather than surfacing an error.
</Warning>

## Custom Field Mapping

The default seeded `NeonMappingRule` records for donations:

| Integration Path | WeGive Path |
| - | - |
| `donorName` | `donor_name` |
| `amount` | `amount` |
| `date` | `created_at` |
| `anonymousType` | `anonymous` |
| `donorCoveredFee` | `fee_amount` |
| `tribute.name` | `tribute_name` |

<Warning>
  These duplicate what `generateDonationParams()` already hardcodes directly — mapping rules only matter for **custom additions** beyond this default set, and (per the standing pull-side gap) only ever apply on push. `generateDonationParams()`'s own mapping-rule loop explicitly excludes `level = 'import'` rules (`->where('level', '!=', 'import')`) — so even the loop itself is intentionally push-scoped; it's not that push accidentally also applies import-only rules. This is a known issue on the pull side.
</Warning>

### Adding Custom Mappings

```json theme={null}
{
  "integration": "DONATION",
  "integration_path": "customField.value",
  "wegive_path": "custom_field_name",
  "crm": "NEON"
}
```

## Synchronization Behavior

### Donation Creation/Update (Push)

1. If `neon_id` already set → `PUT /donations/{neon_id}` (update); otherwise → `POST /donations` (create)
2. **Payment data is only included on the very first push** — see the warning at the top of this page
3. There is no "verify donor account exists first" orchestration step inside donation push itself — if the donor's `neon_account_id` isn't set, the push simply sends `accountId: null` and Neon's own API would reject it
4. There is no "sync campaign first" step either — same pattern, `campaign` is sent `null` if the campaign hasn't separately synced yet

<Warning>
  There is no automatic retry, and no distinction between "modifiable" vs. "immutable" fields enforced by WeGive — a failed update simply fails with whatever error Neon's API returns.
</Warning>

### Import from Neon CRM (Pull)

1. `POST /donations/search` with a last-modified-date filter and an `Email NOT_BLANK` filter
2. Matches the donation's Neon account to an already-imported WeGive donor
3. Creates a new WeGive `Transaction` for any Neon donation not already correlated by `neon_id`

<Warning>
  Pull does **not** use `NeonMappingRule` at all — same gap as accounts and campaigns. This is a known issue.
</Warning>

## API Examples

### Creating a Donation (real payload shape)

```http theme={null}
POST /v2/donations
{
  "accountId": "67890",
  "donorName": "Jane Doe",
  "amount": 250.00,
  "date": "2026-01-20T14:30:00Z",
  "campaign": null,
  "tribute": {
    "name": null,
    "type": "Honor"
  },
  "anonymousType": false,
  "donorCoveredFee": 7.50,
  "sendAcknowledgeEmail": false,
  "payments": [
    {
      "id": null,
      "check": {
        "accountType": "Checking",
        "accountNumber": "9876",
        "institution": "Community Bank",
        "accountOwner": "Jane Doe"
      },
      "amount": 250.00,
      "creditCardOffline": null,
      "note": "https://dashboard.wegive.com/payments/donations/12347",
      "tenderType": 3
    }
  ]
}
```

<Note>
  This reflects the real payload shape: `anonymousType` as a raw boolean, `tribute.type` always `"Honor"`, no `currency` key, and `campaign`/`check` or `creditCardOffline` sent as `null` when not applicable rather than omitted.
</Note>

### Updating a Donation (payments omitted)

```http theme={null}
PUT /v2/donations/12345
{
  "accountId": "67890",
  "donorName": "Jane Doe",
  "amount": 250.00,
  "donorCoveredFee": 5.00,
  "tribute": {
    "name": "Updated tribute name",
    "type": "Honor"
  },
  "payments": null
}
```

### Searching Donations (Pull)

```http theme={null}
POST /v2/donations/search
{
  "searchFields": [
    {"field": "Donation Created Date", "operator": "GREATER_THAN", "value": "2026-01-01"},
    {"field": "Email", "operator": "NOT_BLANK"}
  ],
  "outputFields": ["Donation Amount", "Account ID", "Donation ID", "Payment ID", "Donation Created Date"],
  "pagination": {"currentPage": 0, "pageSize": 200}
}
```

## Error Handling

<Warning>
  There is no automatic retry, amount range validation, or "account creation on missing account" recovery logic performed by WeGive for donation push. A failed push simply fails — check Sentry for the specific Neon API error.
</Warning>

## Best Practices

* Understand that editing a donation's tribute/fee/campaign after initial creation will **not** re-push its payment/tender details — if the payment record needs correcting in Neon, it has to be corrected directly in Neon, not via a WeGive-side re-sync
* Don't expect `tribute.type` to distinguish memorial vs. honor gifts — it's always `"Honor"`
* A donation with no campaign correlation in Neon yet will push with `campaign: null` — sync/verify the campaign separately first if attribution matters

## Related Documentation

* [Account Mapping](/external/onboarding/neon/data-mapping/account)
* [Data Mapping Overview](/external/onboarding/neon/data-mapping/overview)


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