Skip to main content

Upsert Contact

Create a contact, or update it if one already exists — in a single request.

Behaviour

match_on selects the key used to find an existing contact: external_id or email. email is always required — it is the contact's identity and the data used when a new contact is created.

  • If no contact matches, a new one is created and the response is 201 Created.
  • If a contact matches, it is updated and the response is 200 OK.

The response body is the full contact in both cases; the status code tells you which branch ran. Repeating the same request converges on the same contact rather than creating duplicates.

Merge rules on the update branch

When an existing contact is matched, only the fields you include change:

  • A field you include is set to the value you send.
  • A field you omit is left unchanged.
  • A nullable field sent as null (or "") is cleared.
  • custom_fields merge: a field sent with value: null is deleted; fields you omit are left unchanged.
  • contact_lists are added; existing memberships are never removed.

email cannot change on the update branch. When you match by external_id and send a different email, the matched contact keeps its existing email and the email in the body is ignored.

Request

POST /contacts/upsert

Request Body

ParameterTypeRequiredDescription
match_onstringYesWhich key identifies an existing contact: external_id or email.
emailstringYesContact's email address (valid email format). Always required, including when matching by external_id.
external_idstring|nullWhen match_on is external_idExternal identifier (max 255 characters). Must be unique within your account.
first_namestring|nullNoContact's first name (max 255 characters).
last_namestring|nullNoContact's last name (max 255 characters).
phonestring|nullNoContact's phone number (max 32 characters).
languagestring|nullNoISO 639-1 language code (2 characters, e.g. "en", "pl").
country_codestring|nullNoISO 3166-1 alpha-2 country code (2 characters, e.g. "US", "PL").
timezone_uuidstring|nullNoUUID of the timezone to assign to the contact, from List Timezones. Follows the merge rules above on the update branch (omit to leave unchanged, null to clear). An unknown UUID returns 404. Read it back with Get Contact.
contact_listsarray of stringsNoUUIDs of lists to add the contact to. Additive — listed lists are added, existing memberships are never removed. Each must reference an existing list.
custom_fieldsarrayNoCustom field values to set (see Custom Fields section below).

Example — match by external_id

Find the contact whose external_id is customer_123; create it if none exists.

curl -X POST "https://email.easy.tools/api/v1/contacts/upsert" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"match_on": "external_id",
"external_id": "customer_123",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"country_code": "US"
}'

Example — match by email

Find the contact whose email is john@example.com; create it if none exists. When matching by email, you can also set external_id on the matched contact.

curl -X POST "https://email.easy.tools/api/v1/contacts/upsert" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"match_on": "email",
"email": "john@example.com",
"external_id": "customer_123",
"language": "pl",
"contact_lists": [
"9f3c2a1e-8b7d-4e2f-a1c6-2d4f5e6a7b8c"
]
}'

Custom Fields

Set custom field values by including the custom_fields array. Each field_key must reference a custom field that already exists in your account; unknown keys are rejected with 422.

FieldTypeRequiredDescription
field_keystringYesThe unique key of the custom field.
valuemixedYes (key must be present)The value to set (type depends on the field type). On the update branch, send null or "" to delete the value; omit a field to leave it unchanged.

The accepted value for each field type — including the supported date and datetime formats — is the same as for Create Contact.

curl -X POST "https://email.easy.tools/api/v1/contacts/upsert" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"match_on": "email",
"email": "john@example.com",
"custom_fields": [
{"field_key": "company", "value": "Acme Inc"},
{"field_key": "plan", "value": "enterprise"},
{"field_key": "old_field", "value": null}
]
}'

Response

New Contact (201 Created)

{
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": null,
"external_id": "customer_123",
"language": null,
"country_code": "US",
"lists": [],
"custom_fields": [],
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}

Existing Contact Updated (200 OK)

{
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+48123456789",
"external_id": "customer_123",
"language": "pl",
"country_code": "US",
"lists": [
{
"uuid": "9f3c2a1e-8b7d-4e2f-a1c6-2d4f5e6a7b8c",
"name": "Newsletter"
}
],
"custom_fields": [
{
"field_key": "company",
"value": "Acme Inc"
}
],
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-16T14:00:00Z"
}
}

Response Fields

The response is the full contact representation, identical to Get Contact and Create Contact.

FieldTypeDescription
uuidstringUnique identifier for the contact
emailstringContact's email address
first_namestring|nullContact's first name
last_namestring|nullContact's last name
phonestring|nullContact's phone number
external_idstring|nullExternal identifier for integration purposes
languagestring|nullISO 639-1 language code
country_codestring|nullISO 3166-1 alpha-2 country code
listsarrayLists the contact is subscribed to. Each entry has uuid and name.
custom_fieldsarrayCustom field values set on the contact. Each entry has field_key and value.
created_atstringISO 8601 timestamp (UTC)
updated_atstringISO 8601 timestamp (UTC)

Error Responses

Not Found Error (404) — Contact Lists

Returned when one or more UUIDs in contact_lists do not exist for your account.

{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Contact lists not found: 9f3c2a1e-8b7d-4e2f-a1c6-2d4f5e6a7b8c"
}
}

Not Found Error (404) — Timezone

Returned when timezone_uuid does not match a timezone from List Timezones. The contact is neither created nor updated in this case.

{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Timezone not found"
}
}

Duplicate Contact Error (409 Conflict)

Returned when a unique key in the body belongs to a different contact than the one you matched on:

  • match_on is external_id, no contact has that external_id, but the email in the body already belongs to another contact.
  • match_on is email, a contact is matched, but the external_id in the body is already used by another contact.
{
"error": {
"code": "DUPLICATE_RESOURCE",
"message": "Contact with email 'john@example.com' already exists"
}
}

Validation Error (422 Unprocessable Entity)

Returned when match_on is missing or not one of external_id / email, when external_id is missing while match_on is external_id, or when another field fails validation.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid",
"details": {
"match_on": ["The selected match on is invalid."],
"external_id": ["The external id field is required when match on is external_id."]
}
}
}

Custom Field Validation Error (422 Unprocessable Entity)

Returned when a value in custom_fields cannot be applied — for example an unknown field_key, a select value outside the field's options, a value of the wrong type, or a text value over 255 characters. The specific reason is in details.custom_fields.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Custom field validation failed",
"details": {
"custom_fields": ["Custom field 'plan' value 'invalid_option' is not a valid option. Allowed options: free, pro, enterprise"]
}
}
}