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

# Upsert contact

> Create or update a contact via the Nedzo REST API. Matches existing active contacts by phone first, falling back to email.

Create or update a contact based on phone (primary) or email (fallback) matching against active (non-deleted) contacts. Returns `201` for new contacts and `200` for updates.

At least one of `phone` or `email` must be provided.

<Note>
  Matching only considers active contacts. If the only match is a soft-deleted contact, it is ignored and a brand new contact is created — the deleted record stays deleted with its data intact.
</Note>

<Note>
  By default, on a match, **provided** values in the request overwrite the existing contact's scalar fields and custom field values; fields you omit from the request are left untouched. Set `preserveExisting: true` to merge instead — only empty fields and missing custom field values are filled. **Tags are always additive** regardless of `preserveExisting`.
</Note>

<Tip>
  `workspaceId` is required for **account-scoped** API keys and inferred from the key for **workspace-scoped** keys. See [Workspace ID and Request Scope](/authentication#workspace-id-and-request-scope).
</Tip>

<ParamField body="workspaceId" type="string">
  Workspace UUID (required for account API keys, optional for workspace API keys)
</ParamField>

<ParamField body="firstName" type="string">
  First name
</ParamField>

<ParamField body="lastName" type="string">
  Last name
</ParamField>

<ParamField body="email" type="string">
  Email address (used for matching if phone is not provided)
</ParamField>

<ParamField body="phone" type="string">
  Phone number in E.164 format (primary matching field)
</ParamField>

<ParamField body="timezone" type="string">
  IANA timezone identifier (e.g. `America/New_York`). Used by features that respect the contact's local time.

  * **Only applied when creating a new contact.** When matching an existing contact, the existing timezone is preserved (or merged per `preserveExisting`); auto-derivation does not run on updates.
  * On create, when omitted, the timezone is auto-derived from the phone number with the workspace default as fallback. Caller-supplied values always win.
  * Must be a valid IANA timezone identifier; bogus strings are rejected with `400 Bad Request`.
</ParamField>

<ParamField body="preserveExisting" type="boolean" default="false">
  When `true`, populated fields on the matched contact are preserved — only empty fields and missing custom field values are filled in. When `false` (default), the request values overwrite the existing fields and custom field values. Tag attachment is always additive regardless of this flag.
</ParamField>

<ParamField body="tags" type="string[]">
  Tag names to attach to the contact. Tags that don't exist yet are auto-created in your workspace. Tags are always **additive** — this endpoint never removes a tag from a contact, regardless of `preserveExisting`. Names are matched case-sensitively, trimmed, and must be 1–255 characters. Tags in the response are returned in alphabetical order.
</ParamField>

<ParamField body="customFields" type="object">
  Custom field values keyed by the custom variable name **as displayed in the dashboard** (e.g. `"Lead Source"` — including spaces and original casing; not a slug). Accepts `string`, `number`, `boolean`, or `null` (null clears a stored value). Behavior on a match follows `preserveExisting`: when `false` (default), provided values overwrite; when `true`, only fill custom fields the contact doesn't have a value for. Unknown keys are rejected with a `400`. Values are persisted as text and returned as strings.
</ParamField>

<Note>
  This endpoint **strictly validates the request body**. Unknown top-level fields are rejected with `400 Bad Request`. Custom field values must use the `customFields` object.
</Note>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.nedzo.ai/v1/contacts/upsert" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "workspaceId": "789e4567-e89b-12d3-a456-426614174000",
        "phone": "+14155551234",
        "email": "john@example.com",
        "firstName": "John",
        "lastName": "Doe",
        "tags": ["vip"],
        "customFields": {
          "Lead Source": "Website",
          "MRR": 250
        }
      }'
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "workspaceId": "789e4567-e89b-12d3-a456-426614174000",
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com",
    "phone": "+14155551234",
    "timezone": "America/Los_Angeles",
    "tags": ["vip"],
    "customFields": {
      "Lead Source": "Website",
      "MRR": "250"
    },
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:30:00Z"
  }
  ```
</ResponseExample>
