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

# Integration Nuances

> Understand platform-specific behaviors, limitations, and best practices for the DonorPerfect integration.

# DonorPerfect Integration Nuances

Understanding the specific behaviors and limitations of the DonorPerfect integration will help you avoid common issues.

<Warning>
  Several dashboard sync toggles (`crm_sync`, `two_way_sync`, `track_donations`, `track_donors`, `track_recurring_donations`, `track_campaigns`) are currently non-functional — see [Configuration Options](/external/onboarding/donorperfect/configuration-options). This is a known issue. This page describes what the integration actually does regardless of what any toggle shows.
</Warning>

## Platform-Specific Behaviors

### DonorPerfect API Characteristics

<AccordionGroup>
  <Accordion title="XML-Over-HTTP Communication">
    **Legacy API Format**

    DonorPerfect's `xmlrequest.asp` endpoint is called via a single GET request per action, with the action and params encoded directly into the query string. Responses are XML; WeGive parses them with `simplexml_load_string`. There's no JSON option.
  </Accordion>

  <Accordion title="SQL-Style Query Interface">
    **Direct Query Access**

    Pull operations use literal SQL-style `select * from dpgift where ...`/`select * from dp where ...` strings built directly into the request — not a REST-style filtered endpoint. This is genuinely how DonorPerfect's API works for this integration, not an abstraction WeGive built.
  </Accordion>

  <Accordion title="No Rate Limiting or Retry Logic">
    **Contrary to what you might expect**

    There is no rate-limit handling, throttling, or automatic retry anywhere in this integration's code. Every API call (`makeRequest()`) is a single unretried HTTP GET with a 120-second timeout. A failed call is reported to Sentry and the record it was attempting to sync is simply skipped for that run.
  </Accordion>
</AccordionGroup>

## Data Format Requirements

### Date Formatting

<Warning>
  **DonorPerfect dates are sent as MM/DD/YYYY** (via PHP's `->format('m/d/Y')`) for gift dates and pledge start dates.
</Warning>

| Data Type | DonorPerfect Format | Notes |
| - | - | - |
| **Gift Date** | MM/DD/YYYY | Set from the WeGive transaction's `created_at` |
| **Pledge Start Date** | MM/DD/YYYY | Set from the scheduled donation's `created_at` — not the actual pledge frequency start |

<Note>
  There's no donor birth-date field mapped at all in this integration — `exportDonor()` doesn't send one.
</Note>

### Currency Handling

**Amount Conversion (WeGive → DonorPerfect):**

* Gift amount: `(transaction.amount - transaction.fee) / 100` — fee is deducted **before** the cents-to-dollars conversion, not just before sending
* Pledge amount: `(scheduled_donation.amount + optional fee_amount) / 100`

**Currency field:** the transaction's own `currency` value is sent as-is on gift sync (`@currency` param) — there's no currency conversion, exchange-rate handling, or USD-only restriction in the code. Whether DonorPerfect's UI displays a non-USD value meaningfully is outside WeGive's control.

### Text Fields

<Note>
  There is no character-limit truncation logic anywhere in `DonorPerfectIntegration.php` — no `substr`, `Str::limit`, or similar. If DonorPerfect's own API rejects an over-length field, that shows up as a sync failure (reported to Sentry), not a silent truncation.
</Note>

## Synchronization Behaviors

### Incremental Pull Logic

* Pull queries filter on `created_date` then `modified_date` as two separate passes, each starting from `id > 0` and paging forward by recursing with the last-seen id as the new floor — not true pagination with page numbers
* A DonorPerfect row is skipped (not partially synced) if: its email is missing/invalid (donors), or its `amount` is `0`/`'0'` (gifts)
* Records already recognized as WeGive-originated (via `user_id = 'WeGive'` or a `WG:txn:` reference prefix) are skipped on pull — this prevents WeGive from re-importing its own pushes as new records

### Push Logic

* Donor/gift/pledge push is triggered by the model being created or updated in WeGive — this part is genuinely real-time
* A push with no existing `dp_id` creates a new DonorPerfect record; a push with an existing `dp_id` updates it
* All pushed records are stamped `user_id = 'WeGive'` for tracking and later dedup

### Duplicate Handling

The **only** dedup mechanism is DonorPerfect ID (`dp_id`)-based:

* Import skips a gift already recorded against a donor with that exact `dp_id`
* A WeGive-originated gift is recognized by `user_id`/reference prefix, not by amount/date matching
* There's a specific phantom-gift recovery path: if a `dp_savegift` push returns a 2xx response with no `id` field, the integration looks up the gift by its WeGive reference token before retrying, to avoid creating duplicate gift records on a lost-response retry — see the dedicated duplicate-gift-import doc for the full mechanics

<Note>
  There is no email-based or name-based duplicate donor detection — only `dp_id` correlation. A donor pushed before being matched to an existing DonorPerfect person will always create a new one.
</Note>

## Field Mapping Limitations

### Hardcoded Values

| Field | Value Sent | Source |
| - | - | - |
| **Country** (donor address) | `"US"` | Hardcoded in `exportDonor()`, not derived from the donor's actual address |
| **Record Type** (gift) | `"G"` | Hardcoded in `syncGift()` |
| **Frequency** (pledge) | `"M"` (Monthly) | Hardcoded in `syncScheduledDonation()` — **the actual WeGive recurring frequency (weekly, quarterly, annual, etc.) is not sent at all.** Same bug pattern already fixed for NPC/Salesforce, not DonorPerfect — this is a known issue |
| **User ID** | `"WeGive"` | Consistent across donor/gift/pledge/fund pushes, used for the dedup/skip logic above |
| **GL Code field name** | `"GL_CODE"` | Hardcoded in `syncFund()` |

### Missing Field Support

**Fields that don't sync to DonorPerfect:**

* Custom donor profile fields (no custom-field mechanism exists at all)
* Detailed payment method data (card type, last-4, etc.) — DonorPerfect gets amount/date/reference only
* Middle name, suffix, title, salutation — all sent as `null` on push (see the donor data-mapping page)
* Campaign attribution of any kind

**DonorPerfect fields not imported to WeGive:**

* Fund goal amount, category, hierarchy
* Volunteer tracking, relationship/household data
* Custom code tables

## Performance Considerations

### Large Database Handling

Organizations with large DonorPerfect databases should expect longer initial pull times — the pull recurses through records via ascending id, one page of results per API call, with no configurable batch size.

<Note>
  There is no automatic batch-failure retry, no automatic memory-management optimization, and no real-time dashboard progress indicator specific to this integration beyond the generic pull-progress tracking shared by all WeGive CRM integrations (visible as overall pull status, not DonorPerfect-specific detail).
</Note>

## Best Practices

### Data Preparation

<Steps>
  <Step title="Clean Existing Data">
    Remove duplicate donors before initial sync — there's no automated dedup beyond `dp_id` correlation
  </Step>

  <Step title="Validate Email Addresses">
    Donors without a valid email are silently skipped on every pull, not just the first one
  </Step>

  <Step title="Set Up GL Codes First">
    A fund needs a `dp_id` (via `syncFund()`, which only fires automatically the first time a gift references that fund) before gift-fund attribution works
  </Step>
</Steps>

### Ongoing Management

* Check Sentry (not the WeGive dashboard) for DonorPerfect sync failures — there's no customer-facing sync-status view for this integration
* If a transaction later fails after already syncing successfully to DonorPerfect, resyncing from WeGive cannot correct it (the sync payload carries no status field) — see the dedicated status-field doc

## Common Limitations

* **Recurring frequency**: every WeGive recurring donation syncs to DonorPerfect as `"M"` (Monthly), regardless of its actual frequency
* **No fund pull**: funds only push (GL code, one-way); DonorPerfect-side fund changes never flow back
* **No campaign sync**: none at all, despite a dashboard toggle suggesting otherwise
* **Single currency assumption in UI messaging only**: the code itself passes through whatever currency the transaction has; multi-currency correctness on DonorPerfect's side is not something WeGive verifies

## Troubleshooting Common Issues

<AccordionGroup>
  <Accordion title="API Authentication Errors">
    **Symptoms**: DonorPerfect returns an `<error>` element in its response body (DP returns HTTP 200 even on logical errors)

    **Solutions**:

    * Verify the API key is correct and active
    * Check the API key's permissions in DonorPerfect directly
  </Accordion>

  <Accordion title="Missing/Skipped Donors">
    **Symptoms**: A DonorPerfect donor never appears in WeGive after pull

    **Solutions**:

    * Check the donor has a valid, present email in DonorPerfect — this is a hard skip condition, not a warning
  </Accordion>

  <Accordion title="Duplicate-Looking Gift Records">
    **Symptoms**: Two WeGive transactions both reference DonorPerfect, but only one gift is visible in DonorPerfect

    **Solutions**:

    * This usually indicates the two `dp_id`s were genuinely distinct at sync time, and one was later merged/deleted in DonorPerfect itself — not a WeGive-side import bug. See the dedicated duplicate-gift diagnosis doc for the full runbook.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup>
  <Card title="Data Mapping Reference" href="/external/onboarding/donorperfect/data-mapping/overview">
    Detailed field mapping documentation
  </Card>

  <Card title="Get Support" href="mailto:support@wegive.com">
    Contact our team for integration assistance
  </Card>
</CardGroup>


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