Skip to main content

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.

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:
    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.
  4. Unarchive before editing an archived record. A PATCH that sets lifecycle_stage on an archived contact is refused until you confirm:
    Retrying the same PATCH without unarchive: true fails identically; the flag is the confirmation, not a different endpoint.

Decision table