---
title: "Create or update a contact"
method: POST
path: "/api/contacts.upsert"
---

# Create or update a contact

`POST /api/contacts.upsert`

Creates a new contact or updates an existing one. Contacts are matched **by email address only** — the email is the contact's unique identifier. `external_id` is stored but never used for matching, so sending an existing `external_id` with a new email address creates a second contact.

To change a contact's email address: upsert a contact with the new email and the full profile, re-create its list subscriptions (preserving unsubscribed/bounced/complained statuses — do not reset opt-outs), then delete the contact with the old email using `contacts.delete`. Message history and segment memberships are keyed to the email address and are not transferred.

Returns information about whether the contact was created or updated.

## Request body

- UpsertContactRequest
  - `workspace_id` string, required — The ID of the workspace
  - `contact` ContactInput, required
    - `email` string, email, required — Email address of the contact. This is the contact's unique identifier — upserts always match contacts by email address.
    - `external_id` string, nullable — Optional identifier for the contact in your own system (e.g. your user ID). Stored and searchable, but not unique and never used to match contacts during upserts — sending an existing external_id with a new email address creates a second contact.
    - `timezone` string, nullable — Timezone of the contact
    - `language` string, nullable — Preferred language of the contact
    - `first_name` string, nullable — First name of the contact
    - `last_name` string, nullable — Last name of the contact
    - `full_name` string, nullable — Full name of the contact (for systems that don't have separate first/last names)
    - `phone` string, nullable — Phone number of the contact
    - `address_line_1` string, nullable — First line of address
    - `address_line_2` string, nullable — Second line of address
    - `country` string, nullable — Country of the contact
    - `postcode` string, nullable — Postal code
    - `state` string, nullable — State or province
    - `job_title` string, nullable — Job title of the contact
    - `custom_string_1` string, nullable — Custom string field 1
    - `custom_string_2` string, nullable — Custom string field 2
    - `custom_string_3` string, nullable — Custom string field 3
    - `custom_string_4` string, nullable — Custom string field 4
    - `custom_string_5` string, nullable — Custom string field 5
    - `custom_number_1` number, float, nullable — Custom number field 1
    - `custom_number_2` number, float, nullable — Custom number field 2
    - `custom_number_3` number, float, nullable — Custom number field 3
    - `custom_number_4` number, float, nullable — Custom number field 4
    - `custom_number_5` number, float, nullable — Custom number field 5
    - `custom_datetime_1` string, date-time, nullable — Custom datetime field 1
    - `custom_datetime_2` string, date-time, nullable — Custom datetime field 2
    - `custom_datetime_3` string, date-time, nullable — Custom datetime field 3
    - `custom_datetime_4` string, date-time, nullable — Custom datetime field 4
    - `custom_datetime_5` string, date-time, nullable — Custom datetime field 5
    - `custom_json_1` object, nullable — Custom JSON field 1 (must be a JSON object or array)
    - `custom_json_2` object, nullable — Custom JSON field 2 (must be a JSON object or array)
    - `custom_json_3` object, nullable — Custom JSON field 3 (must be a JSON object or array)
    - `custom_json_4` object, nullable — Custom JSON field 4 (must be a JSON object or array)
    - `custom_json_5` object, nullable — Custom JSON field 5 (must be a JSON object or array)

## Response `200`

Contact upserted successfully

- UpsertContactResponse
  - `email` string, email — Email address of the contact
  - `action` 'create' | 'update' | 'error' — The action that was performed: 'create' for new contacts, 'update' for existing contacts, 'error' if validation failed
  - `error` string, nullable — Error message if the operation failed

## Other responses

- `400` — Bad request - validation failed
- `401` — Unauthorized - invalid or missing authentication token
- `500` — Internal server error

## Changes

- **2025-12-02** `c9edbbd76e08` — 4 warning
  - removed the request property `contact/contact_lists`
  - removed the request property `contact/contact_segments`
  - removed the request property `contact/created_at`
  - removed the request property `contact/updated_at`
- **2025-12-01** `bca4a43d9d46` — 1 warning, 3 info
  - added the new `error` enum value to the `action` response property for the response status `200`
  - added the new optional request property `contact/contact_segments`
  - added the new optional request property `contact/full_name`
  - added the optional property `error` to the response with the `200` status
- **2025-11-30** `f018ea668217` — 4 warning, 4 info
  - removed the optional property `contact` from the response with the `200` status
  - removed the optional property `error` from the response with the `200` status
  - added the new `create` enum value to the `action` response property for the response status `200`
  - added the new `update` enum value to the `action` response property for the response status `200`
  - …4 more
- **2025-11-29** `c39acad12e49` — 6 warning
  - removed the request property `contact/last_order_at`
  - removed the request property `contact/lifetime_value`
  - removed the request property `contact/orders_count`
  - removed the optional property `contact/last_order_at` from the response with the `200` status
  - …2 more
- …earlier changes not shown

[Full history](https://skmtc.dev/pierre-b/apis/notifuse-api/changes/api/contacts.upsert/post.md)

---

[API](https://skmtc.dev/pierre-b/apis/notifuse-api.md) · [All operations](https://skmtc.dev/pierre-b/apis/notifuse-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/pierre-b/notifuse-api/revisions/d26605f2fc47/schema)
