POST /api/Contact/Transaction (the recommended path for partner integrations) and POST /api/Contact (the direct path).
If you have not read the Contacts concept page yet, start there — it covers the data model and the duplicate-prevention reasoning that drives this workflow.
Scenario
Your platform has a new donor who needs to exist as a Contact in your customer’s Virtuous organization. You have their name, email, optionally an address, and an identifier from your platform. The naive path —POST /api/Contact with the donor data — works once and then produces duplicates the second time the same donor passes through your platform. This workflow shows the safe path.
Prerequisites
- A valid CRM+ API token — see Authentication.
- The Contact data from your platform: name, email or phone, and your platform’s identifier for the donor.
- The base URL:
https://api.virtuoussoftware.com. For partner integrations, use the customer’s Seeded Sandbox during development — see Base URLs and Environments.
Step 1: choose the creation pattern
The two patterns differ in matching behavior, response semantics, and timing.| Pattern | Endpoint | Matching | Response | Best for |
|---|---|---|---|---|
| Transaction (recommended) | POST /api/Contact/Transaction | Built-in: matches by email, phone, address, name, and external reference | Async — the real Contact is created during the nightly batch | Partner sync from external systems |
| Direct create | POST /api/Contact | None — creates a new Contact unconditionally | Sync — the Contact is created immediately with an ID in the response | Cases where you already know the donor does not exist (e.g., you just searched and found nothing) |
Step 2: pre-create lookup
Even with the Transaction endpoint handling matching automatically, a pre-create lookup is worth doing for two reasons:- If the donor already exists in Virtuous, you may want to use the existing record rather than submitting a Transaction that will simply match against it. This is faster (no waiting for the batch) and cleaner (no orphan Transaction records to inspect).
- If you find an existing Contact, you can capture its
idto store on your side immediately, rather than waiting for the webhook that will eventually confirm the match.
GET /api/Contact/Find is the lookup endpoint. It accepts query parameters for email, reference source, and reference ID:
404 indicates no Contact matched.
GET /api/Contact/Find matches on the exact values supplied. It does not perform fuzzy matching on name or address. If your goal is broader deduplication, fall back to POST /api/Contact/Query with name and postal code filters — but treat the results as suggestions for human review, not automatic merge candidates.Step 3: submit the Contact Transaction
If the lookup returned no existing Contact, submit a Transaction. The endpoint accepts a flat payload describing the donor:200 OK response indicates the Transaction was accepted into the holding state. The endpoint does not return a Contact ID in the body — the real Contact does not yet exist. The nightly batch will resolve the Transaction and either create a new Contact or merge the data into a matching existing record.
Required fields
The CRM+ spec does not enumerate required fields forPOST /api/Contact/Transaction, but the matching algorithm needs enough signal to make a confident decision. At minimum, include:
| Field | Why |
|---|---|
referenceSource and referenceId | The most reliable match signal. Use a stable, unique identifier from your platform. |
firstName + lastName (or name for organizations) | Required for new Contact creation if no match is found. |
email and emailType | Strongest secondary match signal. Include whenever available. |
contactType | Determines whether the new record is a Household, Organization, or Foundation. Defaults to Household if omitted, but explicit is safer. |
Step 4: confirm the Contact was created
Because the Transaction is asynchronous, your integration does not learn the resulting Contact’s ID from the API response. Two patterns confirm the outcome:Pattern A: webhook (recommended)
Subscribe to thecontactCreate and contactUpdate events via POST /api/Webhook. When the nightly batch processes your Transaction, one of these events fires:
contactCreateif a brand-new Contact was created.contactUpdateif the Transaction matched an existing Contact and merged the incoming data.
contactReferences[] array — which contains the referenceSource + referenceId pair you submitted. Match the incoming event to the pending Transaction by that pair, capture the Contact’s id, and store it on your side.
See Webhooks Overview for subscription setup and Event Types for the event payloads.
Pattern B: poll-by-reference
If you cannot use webhooks, pollGET /api/Contact/{referenceSource}/{referenceId} periodically until it returns a Contact:
cURL
404 while the Transaction is still in the holding state and 200 with the Contact record once it has been resolved. Poll on a reasonable interval (every few hours, not every few seconds) — the nightly batch typically runs overnight, so frequent polling is wasted load.
Direct creation (when synchronous is required)
If your integration genuinely needs synchronous Contact creation — for example, an interactive admin tool where a user clicks “Create” and expects a Contact ID back immediately — usePOST /api/Contact:
200 OK (not 201) with the created Contact record in the response body, including the new id. Store the ID on your side immediately — there is no Transaction-style holding state to inspect later.
Error handling
Common error scenarios on Contact creation:| Status | Cause | Action |
|---|---|---|
400 Bad Request | Required fields missing or malformed JSON | Inspect the response body; fix the request. |
401 Unauthorized | Invalid or expired API token | Check the credential; re-issue if needed. See Authentication. |
403 Forbidden | API key permissions insufficient | Check the permission group on the key. |
422 Unprocessable Entity | Validation failed (invalid contact type, malformed email) | Read error.details[] for field-specific messages. See Error Handling. |
429 Too Many Requests | Rate limit exceeded | Back off per the Retry-After header. See Rate Limits. |
End-to-end walkthrough
The complete safe pattern, combining lookup, Transaction submission, and webhook-based outcome detection:Look up the Contact by your platform's reference
Call
GET /api/Contact/Find with your referenceSource and referenceId. If a match is returned, use the existing Contact and skip the rest of the workflow.If no match, fall back to email lookup
Call
GET /api/Contact/Find with the donor’s email address. If a match is returned, decide whether to associate (your reference is stored on the existing Contact via a subsequent update) or treat as a different donor.Submit a Contact Transaction
POST /api/Contact/Transaction with the full donor data. The response is 200 OK with no body — record the submission on your side as “pending.”Wait for the contactCreate or contactUpdate webhook
The nightly batch processes the Transaction and fires the corresponding webhook. Your handler matches the event to your pending submission by the
referenceSource + referenceId pair, captures the Virtuous Contact id, and updates your record from “pending” to “synced.”Fall back to polling if webhooks fail
If you haven’t received the webhook after a reasonable window (a day plus the platform’s nightly batch window), poll
GET /api/Contact/{referenceSource}/{referenceId}. If the Contact exists, capture the ID. If not, the Transaction may have landed in the needs-update bucket and requires manual resolution — see Reconcile Failed Syncs.Where to go next
Update a Contact
The companion workflow — how to safely modify an existing Contact without overwriting other fields.
Create a Donation
Record a Gift on behalf of a Contact — the second core write workflow for partner integrations.
Handle Duplicate Records
What to do when deduplication fails and duplicate Contacts appear.
Build a Two-Way Sync
Combine create, update, and webhook patterns into a continuous sync architecture.