Skip to main content
The Transaction endpoints are how partner integrations should push Contact and Gift data into Virtuous. They differ from the direct create endpoints (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:
  1. 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.
  2. 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.
  3. On match: the incoming data is merged into the existing record (for Contacts) or associated with the existing Contact (for Gifts).
  4. 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.
  5. Webhooks fire when the real Contact or Gift is created, regardless of whether matching succeeded or a new record was made.
The Virtuous team explicitly recommends this pattern for partner integrations in the OpenAPI spec itself. It is the safer default.
If you are starting a new partner integration, build it around the Transaction endpoints by default. Reach for direct create only when you have already independently confirmed (via your own lookup) that the target record exists with a specific Virtuous ID.

The two Transaction endpoints

EndpointWhat it imports
POST /api/Contact/TransactionA single Contact record. Used when your platform creates or updates donor data without an associated gift.
POST /api/v2/Gift/TransactionA single Gift record, optionally with the Contact data embedded. The Contact-matching logic runs on the embedded contact data.
POST /api/v2/Gift/TransactionsA 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.
The Gift Transaction endpoint is the workhorse for partner integrations. It accepts both the gift data and an embedded contact data block, allowing a partner to push a single payload that says “here is a donation made by this donor” and let Virtuous figure out whether the donor already exists.

Sample Gift Transaction request

A typical gift import from an external system:
cURL
curl -X POST https://api.virtuoussoftware.com/api/v2/Gift/Transaction \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionSource": "YourPlatform",
    "transactionId": "donation-9421",
    "contact": {
      "referenceId": "donor-abc123",
      "name": "Bruce Wayne",
      "type": "Household",
      "firstname": "Bruce",
      "lastname": "Wayne",
      "emailType": "Home Email",
      "email": "bruce@wayne.example",
      "phoneType": "Mobile Phone",
      "phone": "555-0100",
      "address": {
        "address1": "1007 Mountain Drive",
        "city": "Gotham",
        "state": "NJ",
        "postal": "07001",
        "country": "US"
      }
    },
    "giftDate": "2024-12-15",
    "giftType": "Cash",
    "amount": "500.00",
    "currencyCode": "USD",
    "batch": "Year-End-2024",
    "giftDesignations": [
      { "projectCode": "CLEAN-WATER", "amountDesignated": "500.00" }
    ]
  }'
The endpoint returns 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.
The /api/v2/Gift/Transaction endpoint accepts either HMAC or OAuth authentication (per the spec). Standard API Key Bearer-token authentication is also accepted in practice. For partner integrations using API Keys, the standard Authorization: Bearer YOUR_API_TOKEN header works on this endpoint.

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:
SignalWhen matched
External reference (referenceSource + referenceId)An existing Contact has the same (source, id) pair stored in contactReferences. The most reliable match.
Email addressThe email exists on an existing ContactIndividual’s contactMethods.
Phone numberThe phone exists on an existing ContactIndividual’s contactMethods.
Name + addressFirst/last name combined with a matching address city/state/postal. Used as a fuzzy fallback.
The match logic favors specificity. A match on external reference takes precedence over a match on email, which takes precedence over a match on name+address. If multiple existing Contacts could match, Virtuous applies its tie-breaking rules and may either pick the best match or flag the transaction for manual review. For Gift Transactions, the algorithm also matches against:
  • Existing RecurringGifts — if the transaction includes a recurringGiftTransactionId that matches an existing schedule, the Gift is associated with that schedule.
  • Existing Pledges — if the transaction includes a pledgeTransactionId that matches an existing pledge, the Gift is recorded as a pledge payment.
  • Designations — projects can be referenced by projectCode, externalAccountingCode, or projectId. 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.
To minimize needs-update fallout, include as many identifying fields as possible in the Contact block: external reference ID, email, phone, and full address. The more signals the matching algorithm has, the higher the auto-resolution rate.

Idempotency through external references

The Transaction endpoints use external reference identifiers as the primary idempotency mechanism. Two fields control this:
ResourceIdempotency fields
ContactreferenceSource + referenceId
GifttransactionSource + transactionId
When you submit a Transaction with the same (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 eventFires when
Contact CreatedA 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 UpdatedAn existing Contact is modified — including when a matched Contact Transaction merged incoming data into the existing record.
Gift CreatedA real Gift record is created — fires after the nightly batch processes a successful Gift Transaction.
Build your integration to subscribe to these events at your endpoint, then correlate incoming webhooks to your in-flight transactions by the (transactionSource, transactionId) pair. See Webhooks Overview.
Polling the API for “did my transaction become a Gift yet” is wasteful and will not give you timely results — the batch typically runs overnight in the organization’s timezone. Use webhooks instead.

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.
In all other cases — and especially in any automated sync from an external system — use the Transaction pattern.
Direct create endpoints return 200 OK (not 201 Created) and do not include a Location header. The created record’s ID is in the response body. Read the body to get the new resource’s ID; don’t rely on the status code or Location header.

Reading the holding state

Two endpoints let your integration inspect the holding state directly, without waiting for the nightly batch:
EndpointUse
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.
These endpoints return 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.
Last modified on May 27, 2026