> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a Contact

> End-to-end walkthrough for creating a Contact in CRM+ — when to use the Transaction endpoint versus direct creation, and how to avoid producing duplicates.

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](/crm/concepts/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](/crm/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](/crm/base-urls).

***

## 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) |

<Tip>
  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.
</Tip>

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:

<CodeGroup>
  ```bash cURL theme={null}
  # 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"
  ```

  ```javascript JavaScript theme={null}
  async function findContact({ email, referenceSource, referenceId }) {
    const params = new URLSearchParams();
    if (email) params.set('email', email);
    if (referenceSource) params.set('referenceSource', referenceSource);
    if (referenceId) params.set('referenceId', referenceId);

    const response = await fetch(
      `https://api.virtuoussoftware.com/api/Contact/Find?${params}`,
      {
        headers: { Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}` },
      }
    );

    if (response.status === 404) return null;          // No match
    if (!response.ok) throw new Error(`Find failed: ${response.status}`);
    return await response.json();
  }
  ```
</CodeGroup>

A successful match returns the existing Contact record. A `404` indicates no Contact matched.

<Note>
  `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.
</Note>

***

## 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:

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```javascript JavaScript theme={null}
  async function submitContactTransaction(donor) {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/Contact/Transaction',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          referenceSource: 'YourPlatform',
          referenceId: donor.platformId,
          contactType: donor.isOrganization ? 'Organization' : 'Household',
          firstName: donor.firstName,
          lastName: donor.lastName,
          emailType: 'Home Email',
          email: donor.email,
          phoneType: 'Mobile Phone',
          phone: donor.phone,
          address1: donor.address?.line1,
          city: donor.address?.city,
          state: donor.address?.state,
          postal: donor.address?.postal,
          country: donor.address?.country ?? 'US',
          originSegmentCode: 'INTEGRATION-IMPORT',
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`Contact Transaction failed: ${response.status}`);
    }
    return response.status; // 200 indicates accepted into the holding state
  }
  ```
</CodeGroup>

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:

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

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:

### Pattern A: webhook (recommended)

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](/crm/webhooks/overview) for subscription setup and [Event Types](/crm/webhooks/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:

```bash cURL theme={null}
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.

<Warning>
  Polling for Transaction outcomes burns rate-limit budget. Subscribe to webhooks instead unless you have a specific reason you cannot. See [Rate Limits](/crm/rate-limits#staying-within-limits).
</Warning>

***

## 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`:

<CodeGroup>
  ```bash cURL theme={null}
  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" }
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  async function createContactDirect(donor) {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/Contact',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          contactType: donor.isOrganization ? 'Organization' : 'Household',
          name: donor.displayName,
          contactIndividuals: [
            {
              firstName: donor.firstName,
              lastName: donor.lastName,
              isPrimary: true,
              contactMethods: [
                { type: 'Home Email', value: donor.email, isPrimary: true, isOptedIn: donor.emailOptIn },
                ...(donor.phone ? [{ type: 'Mobile Phone', value: donor.phone, isPrimary: true }] : []),
              ],
            },
          ],
          address: donor.address && {
            address1: donor.address.line1,
            city: donor.address.city,
            state: donor.address.state,
            postal: donor.address.postal,
            country: donor.address.country ?? 'US',
            isPrimary: true,
          },
          contactReferences: [{ source: 'YourPlatform', id: donor.platformId }],
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`Contact create failed: ${response.status}`);
    }
    return await response.json(); // Contains the new Contact's id
  }
  ```
</CodeGroup>

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.

<Warning>
  `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.
</Warning>

***

## 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](/crm/authentication#authentication-errors).              |
| `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](/crm/error-handling#validation-error-details). |
| `429 Too Many Requests`    | Rate limit exceeded                                       | Back off per the `Retry-After` header. See [Rate Limits](/crm/rate-limits).                                             |

Wrap creation calls in the defensive error-handling pattern from [Error Handling](/crm/error-handling#handling-errors-in-code).

***

## End-to-end walkthrough

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

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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."
  </Step>

  <Step title="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."
  </Step>

  <Step title="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](/crm/workflows/reconcile-failed-syncs).
  </Step>
</Steps>

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Update a Contact" icon="user-pen" href="/crm/workflows/update-a-contact">
    The companion workflow — how to safely modify an existing Contact without overwriting other fields.
  </Card>

  <Card title="Create a Donation" icon="hand-holding-dollar" href="/crm/workflows/create-a-donation">
    Record a Gift on behalf of a Contact — the second core write workflow for partner integrations.
  </Card>

  <Card title="Handle Duplicate Records" icon="copy" href="/crm/workflows/handle-duplicate-records">
    What to do when deduplication fails and duplicate Contacts appear.
  </Card>

  <Card title="Build a Two-Way Sync" icon="arrow-right-arrow-left" href="/crm/workflows/build-a-two-way-sync">
    Combine create, update, and webhook patterns into a continuous sync architecture.
  </Card>
</CardGroup>
