POST /api/Contact, POST /api/Gift) in three important ways: they apply Virtuous’s matching algorithm to avoid duplicates, they support idempotency via external reference identifiers, and they process asynchronously through a nightly batch.
This page covers how the Transaction pattern works, the two transaction endpoints, what happens after you submit, and how to handle the async-by-default nature in your integration.
Why the Transaction pattern exists
Partner integrations face a recurring data-quality problem: an external system has a record of a donor or a donation, and that record needs to flow into Virtuous, but the donor may or may not already exist in Virtuous. Naively creating a new Contact every time produces duplicates; implementing a robust matching algorithm in the partner integration is expensive and error-prone. The Transaction pattern solves this by moving the matching logic into Virtuous. When you submit a Transaction:- The API accepts your payload and places it into a holding state — visible in the Virtuous UI under “Imports” but not yet a real Contact or Gift.
- A nightly batch runs the matching algorithm against existing records: email, phone, address, name, and external reference IDs are compared. The data type — Contact or Gift — determines which match logic applies.
- On match: the incoming data is merged into the existing record (for Contacts) or associated with the existing Contact (for Gifts).
- On no match: a new record is created, or — for Gifts — the transaction is moved to the “needs update” bucket for an administrator to manually resolve.
- Webhooks fire when the real Contact or Gift is created, regardless of whether matching succeeded or a new record was made.
The two Transaction endpoints
| Endpoint | What it imports |
|---|---|
POST /api/Contact/Transaction | A single Contact record. Used when your platform creates or updates donor data without an associated gift. |
POST /api/v2/Gift/Transaction | A single Gift record, optionally with the Contact data embedded. The Contact-matching logic runs on the embedded contact data. |
POST /api/v2/Gift/Transactions | A batch of Gift records (plural). Same semantics as the single-gift endpoint, applied to many records in one request. |
The Gift Transaction endpoints live at
/api/v2/Gift/Transaction and /api/v2/Gift/Transactions — both use the v2 path segment. The Contact Transaction endpoint is at /api/Contact/Transaction with no version segment. This versioning inconsistency is a known spec issue; the live endpoints are stable at the paths shown.Sample Gift Transaction request
A typical gift import from an external system:cURL
200 OK to confirm the transaction was accepted into the holding state. It does not return the created Gift’s ID — because the Gift does not yet exist. The actual creation happens during the nightly batch.
How matching works
The matching algorithm runs during the nightly batch and is the reason this pattern exists. The algorithm considers multiple signals to decide whether an incoming Contact matches an existing one:| Signal | When matched |
|---|---|
External reference (referenceSource + referenceId) | An existing Contact has the same (source, id) pair stored in contactReferences. The most reliable match. |
| Email address | The email exists on an existing ContactIndividual’s contactMethods. |
| Phone number | The phone exists on an existing ContactIndividual’s contactMethods. |
| Name + address | First/last name combined with a matching address city/state/postal. Used as a fuzzy fallback. |
- Existing RecurringGifts — if the transaction includes a
recurringGiftTransactionIdthat matches an existing schedule, the Gift is associated with that schedule. - Existing Pledges — if the transaction includes a
pledgeTransactionIdthat matches an existing pledge, the Gift is recorded as a pledge payment. - Designations — projects can be referenced by
projectCode,externalAccountingCode, orprojectId. The algorithm resolves the reference and creates the appropriate GiftDesignation records.
The “needs update” bucket
When the matching algorithm cannot confidently associate an incoming Gift with a Contact — for example, the transaction has only a name with no email or phone, and no matching name+address record exists — the Gift is moved to a needs update bucket inside Virtuous. Records in the needs-update bucket are visible in the Virtuous UI and require an administrator to manually pick a Contact, create a new one, or discard the transaction. The Gift does not appear in the main donor record or in gift queries until an administrator resolves it. For partner integrations, this has two implications:- You cannot assume submitted Gifts will always appear as real Gift records. Some percentage will land in needs-update awaiting manual resolution. Build your sync’s reconciliation logic to tolerate this gap.
- Your reconciliation should include a check for unresolved transactions. When a customer reports a missing gift, the most common explanation is that the gift landed in needs-update and was never resolved.
Idempotency through external references
The Transaction endpoints use external reference identifiers as the primary idempotency mechanism. Two fields control this:| Resource | Idempotency fields |
|---|---|
| Contact | referenceSource + referenceId |
| Gift | transactionSource + transactionId |
(source, id) pair as a previous submission, Virtuous recognizes it as a duplicate and does not create a second record. This makes it safe to retry a failed transaction submission — the worst case is that the second submission is no-oped.
Practical guidance:
- Set these fields on every Transaction. The cost is one extra field per submission; the benefit is bullet-proof idempotency.
- Use stable, unique identifiers from your platform. A Stripe charge ID, your platform’s internal donation ID, an Eventbrite registration ID — anything that uniquely identifies the source event. Do not use UUIDs you generate fresh on each retry.
- The source should identify your platform. Use a consistent value across all submissions from your integration —
"Stripe","Eventbrite","YourPlatformName". This makes it possible to filter Virtuous data by integration source.
Detecting outcomes — webhooks
Because the Transaction endpoints are asynchronous, your integration cannot rely on the API response to confirm a Gift or Contact was created. The canonical way to detect outcomes is through webhooks:| Webhook event | Fires when |
|---|---|
Contact Created | A real Contact record is created — whether from a new Contact Transaction or as the result of a new Gift Transaction that produced a new Contact. |
Contact Updated | An existing Contact is modified — including when a matched Contact Transaction merged incoming data into the existing record. |
Gift Created | A real Gift record is created — fires after the nightly batch processes a successful Gift Transaction. |
(transactionSource, transactionId) pair. See Webhooks Overview.
When to use direct create instead
The Transaction pattern is the right default, but there are cases where direct create (POST /api/Contact or POST /api/Gift) is appropriate:
- You already have a verified Virtuous Contact ID and you are creating a related sub-resource (a Note, a Tag assignment, a manual gift correction). Matching is not needed because you know the Contact.
- You need synchronous confirmation — for example, an interactive admin tool where a user clicks “Create” and expects the record to appear immediately. The Transaction pattern’s overnight batch is incompatible with this UX.
- You are doing a one-time historical data load that has already been pre-deduplicated by another mechanism, and you have engineered a backstop in case duplicates do appear.
Reading the holding state
Two endpoints let your integration inspect the holding state directly, without waiting for the nightly batch:| Endpoint | Use |
|---|---|
GET /api/Gift/{transactionSource}/{transactionId} | Look up a Gift (or pending Gift Transaction) by your platform’s transactionSource + transactionId. Useful for confirming a transaction was received and for retrieving the resulting Gift after the batch runs. |
GET /api/Contact/{referenceSource}/{referenceId} | The Contact equivalent — look up a Contact by external reference pair. |
404 if no transaction has been submitted with that reference pair, and the matching record (Contact or Gift) if one exists in the system — whether still pending or fully processed.
Where to go next
Sync External Donations into Virtuous
The canonical end-to-end recipe — submitting a Gift Transaction, handling the response, and waiting for the webhook.
Webhooks Overview
Subscribe to Contact Created, Gift Created, and update events to detect Transaction outcomes.
Handle Duplicate Records
What to do when matching fails and duplicate records appear.
Reconcile Failed Syncs
Patterns for finding Transactions stuck in the needs-update bucket.
Relationships and IDs
How
referenceSource/referenceId and transactionSource/transactionId work as bridges between your IDs and Virtuous’s.Donations / Gifts
The Gift resource shape — what your Transaction will become after the batch runs.