## TL;DR

HubSpot treats email as the unique key for contacts, so creating a contact whose email already exists returns CONTACT_ALREADY_EXISTS. Search for the contact by email first; if found, update it instead of creating. Make this search-then-write pattern the default in every agent contact flow.

## Error

```text
{
  "status": "error",
  "message": "Contact already exists. Existing ID: 12345678",
  "errorType": "CONTACT_ALREADY_EXISTS"
}
```

## Steps

1. Before any contact create, POST to /crm/v3/objects/contacts/search with a filter on the email property using EQ. Expected: you get either zero results or the existing contact id.
2. If a contact is returned, PATCH /crm/v3/objects/contacts/[id] with the new properties instead of creating. Expected: the update returns 200 and no duplicate exists.
3. If nothing is returned, proceed with the create. Expected: the create returns 200 with a new id.
4. For bulk loads, pre-batch: collect all emails, run one search per batch, and split rows into create vs update lists. Expected: zero CONTACT_ALREADY_EXISTS errors across the whole load.
5. Normalize emails (lowercase, trim) before comparing. Expected: "Jane@[domain]" and "jane@[domain]" resolve to the same contact.

## When to use

- Contact create calls return 409 with CONTACT_ALREADY_EXISTS.
- An agent sync loop creates duplicates on retry because it never checks existence first.
- CSV imports report skipped rows for existing emails.

## When not to use

- Company duplicates (HubSpot dedupes companies by domain, different flow).
- ASSOCIATION_LIMIT_REACHED (association caps, not identity).
- You genuinely need two contacts sharing an email (rare; HubSpot still blocks it).

## Tool compatibility

- HubSpot CRM API v3, contacts search and batch endpoints.
- Private apps with crm.objects.contacts read and write scopes.

## Variant phrasings

### Contact already exists. Existing ID

The message form; the returned id is the record to update.

### 409 on contacts batch create

One row in the batch collided; split the batch into creates and updates.

## Why it happens

HubSpot enforces email uniqueness at the platform level for contacts. Unlike Salesforce, there is no duplicate rule to tune; the constraint is fixed, so the application must handle it.

## Edge cases

- Secondary emails do not count; only the primary email property triggers the dedupe.
- Deleted contacts hold the email for a time; a search may return nothing while create still fails, so retry after the purge window.
- Case differences do not create separate contacts, but your pre-check must still normalize case or you will misroute updates.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_sUihYUQrxi_XE0w25tB6ug
