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

# Recurring Donation Mapping

> Detailed field mapping for recurring donations and pledges between WeGive and DonorPerfect systems.

# Recurring Donation Data Mapping

This document details how WeGive `ScheduledDonation` records map to DonorPerfect pledges.

<Warning>
  **Known bug**: every WeGive recurring donation syncs to DonorPerfect with a hardcoded `frequency = "M"` (Monthly), regardless of its actual cadence (weekly, quarterly, semi-annual, annual, 1st-and-15th, etc.). This is a known issue.
</Warning>

## DonorPerfect Table Reference

**Primary Table**: `dpgift` (record type `Pledge`, via `dp_savepledge`) — DonorPerfect doesn't use a separate `dppledge` table for this integration; pledges live in the same gift table under a different record type
**WeGive Model**: `ScheduledDonation`
**Sync Direction**: WeGive → DonorPerfect (Push Only — no pull-back of pledge status or changes)

## Core Pledge Fields

| WeGive Field | DonorPerfect Field | Notes |
| - | - | - |
| `dp_id` | `gift_id` | Correlation key; empty on create, set on update |
| `source.dp_id` | `donor_id` | Donor must already be linked — `syncScheduledDonation()` triggers `exportDonor()` first if not |
| — | `user_id` | Always `"WeGive"` |

## Financial Information

| WeGive Field | DonorPerfect Field | Calculation |
| - | - | - |
| `amount`, `fee_amount` (if `cover_fees`) | `bill` | `(amount + fee_amount if cover_fees else amount) / 100` |
| — | `total` | Always `0` |

<Note>
  Fee handling here is the *opposite direction* from one-time gifts: a one-time transaction's `dp_savegift` **subtracts** the fee to get a net amount; a pledge's `dp_savepledge` **adds** the fee (only if the donor is covering it) to reflect the total amount actually billed. Don't assume the same fee-math applies to both.
</Note>

## Fund Attribution

<Warning>
  **There is no fund/GL code attribution on a pledge at all.** `syncScheduledDonation()`'s `gl_code` parameter is hardcoded to `null` unconditionally — unlike `syncGift()`, it never reads the scheduled donation's fund. Individual payment transactions linked to the pledge (via `syncGift()`) do carry their own `gl_code`, but the pledge record itself carries none.
</Warning>

## Schedule and Frequency

| WeGive Field | DonorPerfect Field | Value |
| - | - | - |
| `created_at` | `gift_date` | `MM/DD/YYYY` |
| `created_at` | `start_date` | `MM/DD/YYYY` — **also** the creation date, not the actual first-charge date |
| — | `frequency` | Always `"M"` (Monthly) — see the bug warning at the top of this page |

## Membership and Miscellaneous Fields

`syncScheduledDonation()`'s full parameter set includes several fields that are **always null**, regardless of any WeGive data: `reminder` set to `"N"`, `solicit_code`, `sub_solicit_code`, `campaign`, `membership_type`, `membership_level`, `membership_enr_date`, `membership_exp_date`, `membership_link_id`, `address_id`, `ty_letter_no`, `vault_id`, `receipt_delivery_g`, `contact_id`, `writeoff_date`. `writeoff_amount` is always `0`. `initial_payment` is always `"N"`.

<Note>
  There's no membership-tracking integration, no campaign attribution, and no writeoff-amount tracking despite these parameters existing in the `dp_savepledge` payload — they're always sent as empty/zero placeholders.
</Note>

## Gift Narrative

Same fixed narrative as one-time gifts: `"Online gift through WeGive"`, no pledge-specific variant.

## Data Flow Process

1. `syncScheduledDonation()` triggers on scheduled-donation create/update (real-time)
2. If the source donor has no `dp_id`, `exportDonor()` runs first
3. Under a per-scheduled-donation DB lock: if `dp_id` already exists, the previous DonorPerfect pledge record is fetched and any locally-null param is backfilled from it before sending an update
4. `dp_savepledge` is called; on success, `dp_id` is stored

<Warning>
  There is no cancellation-specific handling — a scheduled donation being paused, cancelled, or deleted in WeGive doesn't trigger any specific pledge-status update in DonorPerfect. The pledge simply isn't touched again unless the scheduled donation is otherwise updated. There is no `pledge_status` parameter anywhere in `dp_savepledge`'s payload.
</Warning>

## Payment-to-Pledge Linking

Individual payments link to a pledge via the **transaction's** own sync (`syncGift()`), not the pledge's sync:

* `pledge_payment` is set to `"Y"` if the transaction has a `scheduled_donation_id`
* `plink` is set to the linked scheduled donation's `dp_id`, if that scheduled donation has already synced

<Note>
  If a payment is processed before its scheduled donation has ever synced to DonorPerfect (no `dp_id` yet), `plink` is simply `null` for that payment — there's no guaranteed ordering that forces the pledge to sync first. The pledge does eventually get its own `dp_id` from its own independent sync trigger, but a specific payment might race ahead of it.
</Note>

## Error Handling

<Warning>
  Same as every other DonorPerfect sync path: no automatic retry, no exponential backoff. A failed `dp_savepledge` call is reported to Sentry (via `Undefined id`/general exception paths) and the scheduled donation is left without a `dp_id` until the next update triggers another attempt.
</Warning>

## API Operations

### `dp_savepledge`

See the full parameter breakdown above — every field not explicitly listed as WeGive-derived is a fixed placeholder (null, `"N"`, or `0`).

## Data Quality Considerations

### Before Sync

* Understand that pledge frequency will show as Monthly in DonorPerfect regardless of the real schedule (known issue) — factor this into any DonorPerfect-side reporting expectations until that's fixed
* Don't rely on the pledge record for fund/GL code reporting — that lives only on the individual linked payments

### Ongoing Maintenance

* Check Sentry (not the WeGive dashboard) for `dp_savepledge` failures

## Known Limitations

* **Frequency**: always "Monthly" regardless of actual cadence (known issue)
* **Fund attribution**: none on the pledge itself
* **No pledge-status sync**: pause/cancel/resume in WeGive doesn't push any corresponding status change to DonorPerfect
* **No pull-back**: pledge changes made directly in DonorPerfect never flow back to WeGive
* **No membership or campaign data**: fields exist in the payload but are always empty placeholders

## Troubleshooting

### Pledges Not Creating

**Possible causes:**

* The integration is disabled
* The donor sync (triggered inline first) failed
* API authentication failure

**Solutions:**

* Confirm the integration's `enabled` flag
* Check Sentry for the specific failure

### Payments Not Linking to Pledges

**Possible cause:** The scheduled donation hasn't synced yet (no `dp_id`) at the moment the payment itself synced — `plink` will be null for that payment permanently; it isn't retroactively backfilled.

### Pledge Shows Wrong Frequency

**Cause:** Known bug, not a misconfiguration.

## Related Documentation

<CardGroup>
  <Card title="Transaction Mapping" href="/external/onboarding/donorperfect/data-mapping/transaction">
    How individual payments link to pledges
  </Card>

  <Card title="Donor Mapping" href="/external/onboarding/donorperfect/data-mapping/donor">
    Donor record requirements for pledges
  </Card>

  <Card title="Fund Management" href="/external/onboarding/donorperfect/data-mapping/fund">
    GL code setup (used by individual payments, not by pledges themselves)
  </Card>

  <Card title="Configuration Guide" href="/external/onboarding/donorperfect/configuration-options">
    Integration-level configuration (most sync-scope toggles are currently non-functional)
  </Card>
</CardGroup>

For additional help with recurring donation mapping, contact our support team at [support@wegive.com](mailto:support@wegive.com).


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