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

# Upsert by external_id

> The right way to wire create-or-update from your CRM.

The `externalId` field is the link between **your** record identifier
and **our** customer record. Use it deliberately and create-or-update
from your CRM becomes a single API call.

## The rule

* When a CRM event fires (created or updated), call `POST /v1/customers`
  with the same `externalId` every time.
* We will create on first call, update on every subsequent call.
* No bookkeeping needed on your side.

## Why not just call PATCH?

`PATCH` requires our customer id (UUID) or your `externalId` in the
path. If your CRM event fires before we know we have a record (race),
or if you're rebuilding state, you'd have to handle "not found →
create". `POST` with `externalId` handles both cases in one call.

## Picking a good `externalId`

* **Stable.** Use the immutable id from your CRM, not the email (emails
  change).
* **Scoped to your organisation.** Two of your records cannot share an
  `externalId`. Reusing an id is the upsert signal — make sure you mean
  it.
* **Reasonable length.** Up to 255 characters; usual CRM ids fit
  comfortably.

Good examples:

```
hubspot:123456789
salesforce:0035g000001a2BcQAA
internal:customer/7741
```

Bad examples:

```
jane@example.com    # changes when she changes email
1                   # collides easily across data sources
```

## Worked example

CRM webhook fires with the contact:

```json theme={null}
{
  "contactId": "hubspot:123456789",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "+61400000000"
}
```

Send it to Instant Compliance:

```bash theme={null}
curl -X POST https://app.instantcompliance.ai/api/v1/customers \
  -H "Authorization: Bearer ic_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hubspot-contact-123456789-2026-06-23T01:00:00Z" \
  -d '{
    "externalId": "hubspot:123456789",
    "fullName": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+61400000000"
  }'
```

* First call: returns `201 Created` with our UUID.
* Jane updates her phone in HubSpot → webhook re-fires → same call →
  returns `200 OK` with the same UUID.

## Email conflicts

Email alone is not treated as unique — family members often share one
address. A record only matches an existing customer when **both** `email`
and `fullName` (case-insensitive) match. If that matched customer has a
*different* `externalId`, we reject with `409 conflict`:

```json theme={null}
{
  "error": {
    "code": "conflict",
    "message": "A different customer record already uses this name and email address.",
    "details": {
      "conflicting_external_id": "salesforce:0035g000001a2BcQAA",
      "conflicting_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  }
}
```

This usually means two of your data sources have separate ids for the
same person. Resolve in your CRM (merge the contacts), then retry.
