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

> Detailed mapping between Neon CRM campaign objects and WeGive campaign records

# Campaign Object Mapping

This document details the real mapping used by `NeonIntegration::generateCampaignParams()` (push) and `SyncNeon::importCampaigns()` (pull).

## Core Campaign Mapping (Push)

| WeGive Field | Neon Field |
| - | - |
| `name` | `name` |
| `goal` | `goal` |
| `start_date` | `startDate` |
| `end_date` | `endDate` |
| `deleted_at` is not null ? `'INACTIVE'` : `'ACTIVE'` | `status` |
| `slug` (via `{organization}/campaigns/{slug}`) | `campaignPageUrl` |
| `slug` (via `{organization}/campaigns/{slug}/give`) | `donationFormUrl` |
| `neon_id` (WeGive-stored) | `id` |

<Note>
  URLs use the organization's real donor-portal base URL (`donorPortalBaseUrl()`), not a hardcoded `app.wegive.com` — the actual domain varies by organization (custom domains are supported).
</Note>

<Warning>
  There is no `description` field sent — Neon's `pageContent` field exists for this purpose but is commented out in the push code and never populated. Fund, purpose, parent-campaign (hierarchy), and social-fundraising configuration are all commented-out placeholders too — none of them are ever sent, despite Neon's platform supporting all of them.
</Warning>

## Statistics — Pull Behavior Is Much Narrower Than It Looks

<Warning>
  **`importCampaigns()` does not import statistics at all.** It only reads `id`, `name`, and `status` from each Neon campaign in the response — `statistics.donationAmount`, `donationCount`, `eventRegistrationAmount`, `eventRegistrationCount`, `grandTotal`, `pledgeAmount`, `pledgeCount` are never read or written to the WeGive `Campaign` record by this method, despite Neon's own campaign-list response including all of them. Any statistics fields you see referenced elsewhere (seeded `NeonMappingRule` defaults) have no actual consumer — this is a known issue, the same pull-never-applies-mapping-rules gap documented for accounts/donations.
</Warning>

<Warning>
  **`importCampaigns()` also never updates an existing campaign** — it only creates a new WeGive `Campaign` when no `neon_id` match is found. If a campaign's name, goal, dates, or status change in Neon after the initial import, those changes are never reflected back in WeGive on subsequent pulls.
</Warning>

<Warning>
  Campaign push on the `generateCampaignParams()` side also has no `statistics` key sent at all (it's a fully commented-out block) — statistics genuinely are Neon-computed-and-owned data, never overwritten by WeGive on push. The gap is entirely on the pull side, where the data that exists in Neon's response simply isn't read.
</Warning>

## Status Mapping

| WeGive State | Neon `status` |
| - | - |
| Not deleted | `"ACTIVE"` |
| `deleted_at` set | `"INACTIVE"` |

### Status Synchronization

* **Push**: deleted campaigns push as `INACTIVE`
* **Pull**: `importCampaigns()` skips any Neon campaign with `status == 'INACTIVE'` entirely — it's not imported at all, not even as an inactive WeGive campaign
* There is no WeGive-side reactivation flow driven by a Neon-side status flip back to `ACTIVE` — since pull never updates existing campaigns (see above), a status change in Neon on an already-imported campaign has no effect on the WeGive record either way

## Custom Field Mapping

The default seeded `NeonMappingRule` records for campaigns include statistics fields (`statistics.donationAmount` → `total_donated`, etc.) — but since `importCampaigns()` doesn't consult `NeonMappingRule` at all (same as every other Neon object type on pull), these defaults have no effect. This is a known issue.

### Adding Custom Mappings

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

<Warning>
  Even for a custom mapping added here, it would only ever apply on push (`generateCampaignParams()`) — the pull-side gap applies to custom mappings the same as the defaults.
</Warning>

## Synchronization Behavior

### Campaign Creation/Update (Push)

1. If `neon_id` already set → `PUT /campaigns/{neon_id}` (update); otherwise → `POST /campaigns` (create)
2. There is no name-uniqueness check or duplicate-prevention logic performed by WeGive before push — any rejection would come from Neon's own API

### Import from Neon CRM (Pull)

1. `GET /campaigns` — a flat, unfiltered list fetch, not a search/pagination call
2. Skip any campaign with `status == 'INACTIVE'`
3. If no WeGive `Campaign` exists with a matching `neon_id`, create a new one with just `name` and `neon_id` — goal, dates, and status are **not** set on the newly created WeGive campaign either, only on the Neon side of the relationship
4. If a matching campaign already exists, **nothing happens** — no field is ever updated on subsequent pulls

## Campaign-Donation Relationship

* Donation push includes `campaign.id`/`campaign.name`/`campaign.status` only if the campaign **already has a `neon_id`** — there is no "sync the campaign first if it's missing" orchestration inside donation push. A donation for an unsynced campaign simply pushes with `campaign: null`.
* Recurring-donation push has no campaign field at all — see [Recurring Donation Mapping](/external/onboarding/neon/data-mapping/recurring-donation)

## Error Handling

<Warning>
  There is no automatic retry, date-order validation, or goal-value validation performed by WeGive for campaign push. A failed push simply fails — check Sentry for the specific Neon API error. On pull, a failed `GET /campaigns` request is silently skipped (`$campaignRequest->failed()` → early `return`, no exception, no retry).
</Warning>

## API Examples

### Creating a Campaign (real payload shape)

```http theme={null}
POST /v2/campaigns
{
  "campaignPageUrl": "https://giving.example-org.org/example-org/campaigns/spring-fundraiser-2026",
  "donationFormUrl": "https://giving.example-org.org/example-org/campaigns/spring-fundraiser-2026/give",
  "startDate": "2026-03-01",
  "endDate": "2026-05-31",
  "status": "ACTIVE",
  "goal": 50000.00,
  "id": null,
  "name": "Spring Fundraiser 2026"
}
```

<Note>
  No `statistics`, `fund`, `purpose`, `parentCampaign`, or `socialFundraising` keys are sent — confirmed absent from the real payload. The base URL reflects the organization's own donor-portal domain, not a fixed WeGive domain.
</Note>

### Fetching Campaigns (Pull)

```http theme={null}
GET /v2/campaigns
```

<Note>
  This is a flat list fetch with no search filters or pagination parameters — unlike donor/donation pull, which use `POST .../search` with date filters. A large Neon account with many campaigns would return them all in one call.
</Note>

## Best Practices

* Don't expect campaign statistics (donation totals, pledge counts) to ever appear on the WeGive side via this integration — they're Neon-only data that never gets pulled in
* Don't expect a campaign edit made in Neon (renaming, changing goal/dates) to propagate back to WeGive — pull only creates, never updates
* If a campaign needs its goal/dates corrected on both sides, make the change directly in WeGive and let it push — don't rely on Neon as the source of truth for anything beyond the initial name

## Related Documentation

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


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