Integration Nuances
This page documents behaviors of the WeGive Virtuous integration that arise from the shape of the Virtuous API itself, not from WeGive implementation choices. Understanding them helps explain why certain things sync the way they do — and why a small number of operations cannot be reflected at all.Archived Contacts Are Not Reflected in WeGive
When a contact is archived in Virtuous (via Virtuous’s Archive action, orPUT /api/Contact/Archive/:contactId), that archive is not propagated to WeGive. The corresponding WeGive donor remains active until WeGive next tries to push an update for it, at which point the API responds 404 and WeGive surfaces a hard sync failure for that donor (the customer’s archive intent is then honored — WeGive does not silently recreate the contact).
This is a hard constraint imposed by the Virtuous API, not a WeGive product decision. The sections below document every avenue we evaluated for detecting archives and why each one is blocked.
What “reflecting archives” would require
To mirror Virtuous archive state into WeGive, we would need at least one of:- (A) Push signal — a webhook that fires when a contact is archived or unarchived.
- (B) Pull signal, event-shaped — a query for “contacts archived since timestamp X.”
- (C) Pull signal, state-shaped on a scannable endpoint — an
isArchivedfield on a paginated list response we can walk within the API rate limit. - (D) Reactive signal, disambiguating — a per-record probe whose response distinguishes archive from delete from individual-only delete.
Avenue A — Webhooks (push)
ThePOST /api/Webhook request body enumerates every supported event flag:
contactArchive, contactUnarchive, or contactDelete event. Gifts, projects, notes, and events each have explicit delete events; contacts deliberately do not. Archive operations also do not reliably fire contactUpdate, and the contactUpdate payload would not carry archive state even if they did (see Avenue C).
Result: No push signal exists.
Avenue B — Time-bounded archive feed (event-shaped pull)
POST /api/Contact/Query and POST /api/Contact/Query/FullContact accept a single boolean includeArchived in the request body. It toggles whether archived rows appear in the result set — it is not a filter for “only archived” or “archived since X.”
Contact records carry createDateTimeUtc and modifiedDateTimeUtc, but no archivedDateTimeUtc is exposed anywhere in the read schema, so an archive transition feed cannot even be derived by filtering on modified time.
Result: No event-shaped archive feed is available.
Avenue C — isArchived field on a scannable endpoint (state-shaped pull)
This is the only avenue that would be theoretically scalable under Virtuous’s 1,500 requests per minute rate limit: walk the corpus once via a paginated endpoint and read archive state per record.
Every documented Contact read endpoint was checked:
Where
isArchived does appear in the API surface:
- Write payloads —
POST /ContactandPUT /Contactaccept it. WeGive can set it; Virtuous does not echo it back on reads. - Global search response —
Contact/FindreturnsisArchivedper hit, but this is a search endpoint (one query → small ranked result set), not a bulk corpus walker. - Query request body flag —
includeArchivedis a request-side toggle, not a per-record field.
Query/FullContact with take=1000 — does not carry isArchived on the returned rows. There is no scannable endpoint that exposes per-record archive state.
The closest derivable signal is exclusion: walking the corpus with includeArchived: false (the default), then diffing returned IDs against locally-tracked virtuous_contact_id values, and treating any of ours missing from the result set as “archived.” This is the only path Avenue C leaves open, and it is materially worse than a real signal — see § Why the full-corpus presence diff isn’t a real solution below.
Result: No direct state-shaped read at scale. Only indirect, via expensive corpus diffing.
Avenue D — Reactive per-record probe (current behavior)
WeGive’s current behavior, implemented inapp/Integrations/Virtuous.php, is to detect a missing contact reactively on the next outbound push:
When WeGive attempts to update a donor with an existing virtuous_contact_individual_id, it first issues GET /api/ContactIndividual/:id. A 404 is treated as a hard sync failure for that donor (Contact no longer exists in Virtuous), so the customer’s archive intent is honored — WeGive will not silently recreate the contact on the next push.
This signal has structural limits:
- No proactive mirror. The signal only fires when WeGive next tries to push an update for that specific donor. Until then — possibly hours, days, or never — WeGive’s local state still shows the donor as active.
- No timing. WeGive learns “the contact is gone” but not when it was archived. Any downstream behavior depending on archive timestamp cannot be built.
- Not bulk-feasible. Probing every donor individually would consume
N / 60 / 1500minutes of rate budget per org. For a 100k-contact tenant that is ~67 minutes of full-throttle GETs purely for the read, leaving no budget for writes or other syncs. - Ambiguous. A
404can mean three different things — see next section.
The disambiguation problem
A404 on GET /ContactIndividual/:id — or a contact’s absence from a Query/FullContact walk — could mean any of:
- Contact archived via
PUT /api/Contact/Archive/:contactId. Soft delete; reversible. - Contact deleted entirely. There is no top-level public
DELETE /Contact/:idendpoint, but Virtuous can delete contacts internally and the API surfaces the result as a404. - Individual deleted via the separate
Delete an Individualendpoint, while the parent contact remains. Common for households where one member is removed.
mergedIntoContactId on a successful read. WeGive handles that case — see Contact mapping → Merged contacts.)
Why the full-corpus presence diff isn’t a real solution
The only path Avenue C leaves open: periodically (e.g. nightly) page throughPOST /Contact/Query/FullContact with includeArchived: false, build a set of returned contact IDs, then mark every locally-tracked donor whose virtuous_contact_id is absent from that set as “archived in Virtuous.”
This is theoretically buildable but materially worse than a real signal:
- Cost. For a 200k-contact org, that is ~200 paginated requests per sweep, repeated per-org, repeated nightly. Tractable, but a heavy floor on rate budget that must be coordinated with all other Virtuous syncs (donations, scheduled gifts, etc.).
- Still ambiguous. A missing ID could be archived, deleted, or never have existed at the current Virtuous tenant at all (e.g., reconnect-to-different-org cases). All three would be marked the same way.
- No timing fidelity. “Currently archived” but not “was archived at T.” Downstream behavior that depends on archive timestamp (suppression windows, last-touch reporting, audit trails) cannot be built on top.
- No clean unarchive symmetry. If a customer unarchives in Virtuous, the contact reappears on the next sweep — but WeGive may have already fired downstream effects (suppressed communications, marked donor inactive, etc.). Reversibility is awkward.
- Races with
contactUpdatewebhooks. Webhook deliveries arrive between sweeps and modify the same donor records the sweep is reading. Precedence and idempotency would need clear rules. - Operational fragility. A failed or partial sweep produces false-positive archive marks. Recovery requires re-running the sweep and reverting false flags, which itself needs an audit trail.
What would unblock this
The blocker is the Virtuous API surface, not WeGive engineering choice. Either of the following from Virtuous would make a clean implementation possible:- A
contactArchive/contactUnarchive(and ideallycontactDelete) webhook event, or - An
isArchivedfield on the paginatedPOST /api/Contact/Query/FullContactresponse, ideally accompanied byarchivedDateTimeUtc.
Summary
The current behavior — reactive
404 detection on push, surfaced as a hard sync failure so the donor is not silently recreated — is the most we can do with the signals Virtuous provides.
Related
- Contact mapping — full push/pull behavior for contacts, including the merged-contact case.
- Virtuous API Documentation — endpoint reference cited above.