Skip to main content
This workflow walks through creating a new Contact in CRM+ from start to finish: deciding which creation pattern to use, performing a pre-create lookup to avoid duplicates, submitting the create request, and confirming the resulting Contact record. The two paths covered are 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.
PatternEndpointMatchingResponseBest for
Transaction (recommended)POST /api/Contact/TransactionBuilt-in: matches by email, phone, address, name, and external referenceAsync — the real Contact is created during the nightly batchPartner sync from external systems
Direct createPOST /api/ContactNone — creates a new Contact unconditionallySync — the Contact is created immediately with an ID in the responseCases where you already know the donor does not exist (e.g., you just searched and found nothing)
If you are unsure which to use, choose the Transaction pattern. It is the default for partner integrations and removes the deduplication burden from your code. The cost — asynchronous creation through the nightly batch — is rarely a problem for the use cases that drive partner integrations.
This workflow covers the Transaction pattern as the primary path, with the direct-create path covered as a secondary section for the cases where you need synchronous creation.

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 id to 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:
# Look up by email
curl "https://api.virtuoussoftware.com/api/Contact/Find?email=bruce%40wayne.example" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Look up by reference source + ID (your platform's identifier)
curl "https://api.virtuoussoftware.com/api/Contact/Find?referenceSource=YourPlatform&referenceId=donor-bw-001" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
A successful match returns the existing Contact record. A 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:
curl -X POST https://api.virtuoussoftware.com/api/Contact/Transaction \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "referenceSource": "YourPlatform",
    "referenceId": "donor-bw-001",
    "contactType": "Household",
    "firstName": "Bruce",
    "lastName": "Wayne",
    "emailType": "Home Email",
    "email": "bruce@wayne.example",
    "phoneType": "Mobile Phone",
    "phone": "555-0100",
    "address1": "1007 Mountain Drive",
    "city": "Gotham",
    "state": "NJ",
    "postal": "07001",
    "country": "US",
    "originSegmentCode": "INTEGRATION-IMPORT",
    "tags": "New Donor"
  }'
A 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 for POST /api/Contact/Transaction, but the matching algorithm needs enough signal to make a confident decision. At minimum, include:
FieldWhy
referenceSource and referenceIdThe 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 emailTypeStrongest secondary match signal. Include whenever available.
contactTypeDetermines whether the new record is a Household, Organization, or Foundation. Defaults to Household if omitted, but explicit is safer.
Including a complete address improves match quality for donors who share an email with another household member (a common case for spouse-couple records).

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: Subscribe to the contactCreate and contactUpdate events via POST /api/Webhook. When the nightly batch processes your Transaction, one of these events fires:
  • contactCreate if a brand-new Contact was created.
  • contactUpdate if the Transaction matched an existing Contact and merged the incoming data.
Both event payloads include the Contact’s full record and the 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, poll GET /api/Contact/{referenceSource}/{referenceId} periodically until it returns a Contact:
cURL
curl https://api.virtuoussoftware.com/api/Contact/YourPlatform/donor-bw-001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
This endpoint returns 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.
Polling for Transaction outcomes burns rate-limit budget. Subscribe to webhooks instead unless you have a specific reason you cannot. See Rate Limits.

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 — use POST /api/Contact:
curl -X POST https://api.virtuoussoftware.com/api/Contact \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contactType": "Household",
    "name": "Wayne, Bruce",
    "contactIndividuals": [
      {
        "firstName": "Bruce",
        "lastName": "Wayne",
        "isPrimary": true,
        "contactMethods": [
          { "type": "Home Email", "value": "bruce@wayne.example", "isPrimary": true, "isOptedIn": true },
          { "type": "Mobile Phone", "value": "555-0100", "isPrimary": true }
        ]
      }
    ],
    "address": {
      "address1": "1007 Mountain Drive",
      "city": "Gotham",
      "state": "NJ",
      "postal": "07001",
      "country": "US",
      "isPrimary": true
    },
    "contactReferences": [
      { "source": "YourPlatform", "id": "donor-bw-001" }
    ]
  }'
The direct endpoint returns 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.
POST /api/Contact does not perform deduplication. If a Contact with the same email or reference identifier already exists, this endpoint creates a second record — leaving the customer with duplicate Contacts that must be manually merged. Always run a GET /api/Contact/Find first when using direct create, and only proceed to the POST if no match was found.

Error handling

Common error scenarios on Contact creation:
StatusCauseAction
400 Bad RequestRequired fields missing or malformed JSONInspect the response body; fix the request.
401 UnauthorizedInvalid or expired API tokenCheck the credential; re-issue if needed. See Authentication.
403 ForbiddenAPI key permissions insufficientCheck the permission group on the key.
422 Unprocessable EntityValidation failed (invalid contact type, malformed email)Read error.details[] for field-specific messages. See Error Handling.
429 Too Many RequestsRate limit exceededBack off per the Retry-After header. See Rate Limits.
Wrap creation calls in the defensive error-handling pattern from Error Handling.

End-to-end walkthrough

The complete safe pattern, combining lookup, Transaction submission, and webhook-based outcome detection:
1

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

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

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.”
4

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.”
5

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.
Last modified on May 27, 2026