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

# Contact create conflicts: duplicate and archived contacts

> What the 409 duplicate-identifier and archived-contact errors mean when creating or updating a contact, exactly what the dashboard shows, and the fastest recovery path in the dashboard and over the API.

# Contact create conflicts: duplicate and archived contacts

Phone numbers and email addresses are unique per tenant: one live contact
may hold a given phone number or email at a time. When a create collides
with a record you already have, `POST /api/v1/contacts` answers `409` with a
typed code, and the dashboard's create-contact dialog interrupts the flow
with recovery UI instead of a generic failure. When an update tries to move
an archived contact without an explicit confirmation, the API answers `409`
the same way. This page explains what each code means and the shortest path
past it.

## What each 409 means

### `CONTACT_EMAIL_ALREADY_EXISTS` (409)

A live contact in this tenant already stores this email address. The
comparison is case-insensitive and ignores surrounding whitespace, so
`Foo@Example.com` collides with `foo@example.com`; re-typing the address in
a different casing never creates a second contact. The response `details`
name the colliding `field` and, when the existing record could be resolved,
its id in `existing_contact_id`.

### `CONTACT_PHONE_ALREADY_EXISTS` (409)

The same gate on the phone number. The comparison ignores formatting
(spaces, dashes, parentheses, leading zeros), so `+1 555 0100` and
`+15550100` are the same identifier.

### `CONTACT_WHATSAPP_BSUID_ALREADY_EXISTS` (409)

The same gate on the WhatsApp identifier attached to a contact. Another
contact already carries it, so the new record cannot.

### `CONTACT_ARCHIVED_REQUIRES_UNARCHIVE` (409)

An update tried to move a contact out of the `archived` lifecycle stage
without an explicit confirmation. Archiving is how you retire a record
without deleting it, and Orbit refuses an implicit unarchive so a bulk
update or a stale script cannot silently restore archived contacts into
your live audience. To move an archived contact to any other stage, send
the same update with `"unarchive": true` alongside the `lifecycle_stage`
change.

## What you see in the dashboard

The create-contact dialog checks for duplicates before you ever submit, and
again when the API enforces the uniqueness gate:

* **Step 1 duplicate check.** When you press **Next** on the Basic Info
  step, the dialog searches your existing contacts for an exact match on
  the phone number or email you entered. If one matches, the dialog stops
  on step 1 and shows a banner naming the colliding identifier:
  "A contact with this email already exists. Change the email to continue,
  or close this dialog to edit the existing contact." The check is
  best-effort; the server-side gate below stays authoritative.
* **Submit-time banner and toast.** If the create still reaches a 409 (for
  example the identifier was taken between the check and the submit), the
  dialog shows a persistent banner, "A contact with this email already
  exists. Open the existing contact from the toast, or change the email and
  try again.", together with a toast titled **Duplicate email** or
  **Duplicate phone number**. When the API identified the conflicting
  record, the toast carries a **View existing** action that opens that
  contact's page, so you can update the record you have instead of
  re-creating it.
* **Editing clears the warning.** Changing the phone number or email clears
  the banner and re-enables the flow, so you can retry with the corrected
  identifier.
* **Archived contacts.** When an edit would move an archived contact to
  another stage, the dashboard asks you to confirm the unarchive before the
  change applies; confirming sends the `unarchive: true` confirmation for
  you. If you see `CONTACT_ARCHIVED_REQUIRES_UNARCHIVE` from a script or
  integration instead, add the flag yourself (see below).

The dialog resolves duplicates by pointing you at the existing record; it
does not merge records on your behalf. Merging two records that describe
the same person is a separate, deliberate operation described in
[CDP identity resolution](https://docs.orbit.devotel.io/concepts/cdp-identity-resolution.md).

## Resolve a conflict over the API

1. **Read the 409.** `details.field` tells you which identifier collided
   (`phone`, `email`, or `whatsapp_bsuid`). When the API resolved the
   conflicting record, `details.existing_contact_id` is its id and you can
   skip the lookup.
2. **Fetch the conflicting contact.** Without an id, look the record up by
   identifier with `GET /api/v1/contacts` (the `search` query parameter),
   the same lookup the dashboard uses:

   ```bash theme={null}
   curl "https://orbit.devotel.io/api/v1/contacts?search=foo%40example.com" \
     -H "Authorization: Bearer $ORBIT_API_KEY"
   ```

   Remember the email comparison is case-insensitive: a `409` on
   `Foo@Example.com` means a contact storing `foo@example.com` exists.
3. **Update instead of create.** Open the returned contact and update it
   with `PATCH /api/v1/contacts/{id}` rather than minting a second record.
   If both records describe the same person and you want one canonical
   profile, follow the merge and identity-resolution semantics in
   [CDP identity resolution](https://docs.orbit.devotel.io/concepts/cdp-identity-resolution.md).
4. **Unarchive before editing an archived record.** A `PATCH` that sets
   `lifecycle_stage` on an archived contact is refused until you confirm:

   ```bash theme={null}
   curl -X PATCH "https://orbit.devotel.io/api/v1/contacts/cnt_..." \
     -H "Authorization: Bearer $ORBIT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "lifecycle_stage": "active", "unarchive": true }'
   ```

   Retrying the same PATCH without `unarchive: true` fails identically; the
   flag is the confirmation, not a different endpoint.

## Decision table

| Code | HTTP | Cause | Fix |
| - | - | - | - |
| `CONTACT_EMAIL_ALREADY_EXISTS` | 409 | A live contact already holds this email (case-insensitive). | Use `details.existing_contact_id` (or look the email up) and update that contact, or change the new contact's email. |
| `CONTACT_PHONE_ALREADY_EXISTS` | 409 | A live contact already holds this phone number (format-insensitive). | Same recovery on the phone number: update the existing contact or change the new one. |
| `CONTACT_WHATSAPP_BSUID_ALREADY_EXISTS` | 409 | Another contact already carries this WhatsApp identifier. | Update the existing contact; do not re-create it. |
| `CONTACT_ARCHIVED_REQUIRES_UNARCHIVE` | 409 | The update moves an archived contact to another stage without confirmation. | Re-send the same update with `"unarchive": true` alongside `lifecycle_stage`. |

## Related

* [CDP identity resolution](https://docs.orbit.devotel.io/concepts/cdp-identity-resolution.md) — how
  identifiers map to one canonical contact, and merge semantics.
* [ALREADY\_EXISTS](https://docs.orbit.devotel.io/errors/ALREADY_EXISTS.md) — the generic family these
  codes belong to.
* [Error codes](https://docs.orbit.devotel.io/reference/error-codes.md) — the full generated registry.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.