---
updatedAt: 2026-05-29T08:51:20.000Z
agentTools:
  projectIndex: https://docs.whippy.ai/llms.txt
---

# Upsert Contacts

Bulk creates or updates contacts in an organization.

Optionally, it can opt in contacts to all organization channels or specific channels.
Also, the contacts in the list can optionally be associated with a custom object by providing the custom object ID
in the top level of the request body and the properties of the custom object in the `properties` key of each contact.
А contact's relationship with a custom object is always one-to-one.

**Important: The following fields of a contact are unique and also nullable**:
  - `external_id`
  - `phone`
  - `email`

This means that if a contact with the same value for any of these fields already exists in the organization,
the contact will be updated instead of created.
The following update strategy is applied:
- If a contact is present in the payload where the `external_id` conflicts with the `external_id` of an existing contact:
  - If the existing contact has a different `email` and `phone` than the new contact, the existing contact will be updated with the new data and the email and phone will be overwritten, as long as no other contact has the same email or phone.
- If a contact is present in the payload where the `phone` conflicts with the `phone` of an existing contact:
  - If the existing contact has the same `external_id` as the new contact or its value is not set, the existing contact will be updated with the new data.
  - If the existing contact has a different `external_id` than the new contact, a new contact without a phone will be created.
- If a contact is present in the payload where the `email` conflicts with the `email` of an existing contact:
  - If the existing contact has the same `external_id` and `phone` as the new contact or their values are not set, the existing contact will be updated with the new data.
  - If the existing contact has a different `external_id` and `phone` than the new contact, a new contact without an email will be created.


# OpenAPI definition

```json
{
  "components": {
    "schemas": {
      "NotAuthenticatedErrorResponse": {
        "description": "Not Authenticated Error Response schema",
        "example": {
          "errors": [
            {
              "description": "Not authenticated"
            }
          ],
          "status": 401
        },
        "properties": {
          "errors": {
            "description": "Not authenticated error",
            "items": {
              "properties": {
                "description": {
                  "enum": [
                    "Not authenticated"
                  ],
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                }
              },
              "type": "object",
              "x-struct": null,
              "x-validate": null
            },
            "type": "array",
            "x-struct": null,
            "x-validate": null
          },
          "status": {
            "enum": [
              401
            ],
            "type": "integer",
            "x-struct": null,
            "x-validate": null
          }
        },
        "title": "NotAuthenticatedErrorResponse",
        "type": "object",
        "x-struct": "Elixir.DashboardApiWeb.Public.V1.Documentation.Errors.NotAuthenticated",
        "x-validate": null
      },
      "UpsertContactParams": {
        "example": {
          "channels": [
            {
              "id": "b9416fea-0676-402d-a112-c45a8e194203"
            }
          ],
          "contacts": [
            {
              "address": {
                "address_line_one": null,
                "address_line_two": null,
                "city": "Los Angeles",
                "country": "USA",
                "post_code": "90046",
                "state": "CA"
              },
              "birth_date": {
                "day": 1,
                "month": 1,
                "year": 1980
              },
              "default_channel_id": "b9416fea-0676-402d-a112-c45a8e194203",
              "language": "en",
              "name": "New Name",
              "phone": "+14258658940",
              "properties": {
                "employee_id": "9876"
              }
            },
            {
              "email": "LETICIA@GMAIL.COM",
              "name": "Leticia",
              "phone": "818-705-2979"
            },
            {
              "email": "anan@example.com",
              "name": "Anan Albarak",
              "phone": "818-705-2977"
            },
            {
              "default_channel_id": "b9416fea-0676-402d-a112-c45a8e194203",
              "name": "Taracy & Lee Allen",
              "phone": "310-871-5565"
            },
            {
              "address": {
                "address_line_one": null,
                "address_line_two": null,
                "city": "Los Angeles",
                "country": "USA",
                "post_code": "90046",
                "state": "CA"
              },
              "name": "Arleen",
              "phone": "818-981-0606"
            },
            {
              "birth_date": {
                "day": 1,
                "month": 1,
                "year": 1980
              },
              "name": "Zina Asheriyan",
              "phone": "310-892-3339"
            },
            {
              "external_id": "4321",
              "name": "Shamsi Asheriyan",
              "phone": "818-708-3971"
            },
            {
              "external_id": "1234",
              "name": "Fariba rad Asheriyan",
              "phone": "818-469-3882",
              "properties": {
                "employee_id": "1234"
              }
            }
          ],
          "custom_data": {
            "custom_object_id": "ad4dd52e-127e-4173-a7be-c729ac66f5b8",
            "resource": "contact"
          },
          "opt_in_to_all_channels": true
        },
        "properties": {
          "contacts": {
            "description": "List of contacts that would comprise the contact list",
            "items": {
              "description": "Data for a contact included in the contact list.",
              "properties": {
                "address": {
                  "description": "Optional Address details of the Contact",
                  "example": {
                    "address_line_one": "some street 1",
                    "address_line_two": null,
                    "city": "Los Angeles",
                    "country": "USA",
                    "post_code": "90046",
                    "state": "CA"
                  },
                  "properties": {
                    "address_line_one": {
                      "description": "The first line of the address.",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "address_line_two": {
                      "description": "The second line of the address.",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "city": {
                      "description": "The city of the contact.",
                      "example": "Los Angeles",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "country": {
                      "description": "The country of the contact.",
                      "example": "USA",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "post_code": {
                      "description": "The postal code of the contact.",
                      "example": "90046",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "state": {
                      "description": "The state of the contact.",
                      "example": "CA",
                      "type": "string",
                      "x-struct": null,
                      "x-validate": null
                    }
                  },
                  "type": "object",
                  "x-struct": null,
                  "x-validate": null
                },
                "birth_date": {
                  "description": "Birthday details of the Contact.",
                  "example": {
                    "day": 1,
                    "month": 1,
                    "year": 1980
                  },
                  "properties": {
                    "day": {
                      "description": "The day of the month.",
                      "example": 1,
                      "type": "integer",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "month": {
                      "description": "The month of the year.",
                      "example": 1,
                      "type": "integer",
                      "x-struct": null,
                      "x-validate": null
                    },
                    "year": {
                      "description": "The year of the birth.",
                      "example": 1980,
                      "type": "integer",
                      "x-struct": null,
                      "x-validate": null
                    }
                  },
                  "type": "object",
                  "x-struct": null,
                  "x-validate": null
                },
                "default_channel_id": {
                  "description": "The ID of the channel from which the contact should receive messages by default.",
                  "example": "b9416fea-0676-402d-a112-c45a8e194203",
                  "format": "uuid",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "email": {
                  "description": "Contact email",
                  "example": "ziggy.stardust@example.com",
                  "format": "email",
                  "maxLength": 255,
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "external_id": {
                  "description": "The external ID of a contact. It uniquely identifies a contact and\ncan be useful for associating whippy contacts with external systems.\n",
                  "example": "4321",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "language": {
                  "description": "The language preference of the Contact",
                  "example": "en",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "name": {
                  "description": "Contact name",
                  "maxLength": 255,
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "opt_in_email": {
                  "description": "Whether the contact is opted in to email at the organization level",
                  "type": "boolean",
                  "x-struct": null,
                  "x-validate": null
                },
                "opt_in_sms": {
                  "description": "Whether the contact is opted in to SMS at the organization level",
                  "type": "boolean",
                  "x-struct": null,
                  "x-validate": null
                },
                "opt_out_of_all": {
                  "description": "Whether the contact is opted out of all organization-level channel types",
                  "type": "boolean",
                  "x-struct": null,
                  "x-validate": null
                },
                "phone": {
                  "description": "Contact phone number",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "properties": {
                  "description": "Contact properties mapped to custom data. These properties are mapped to the custom object in the payload.\n\nThe properties keys should correspond to the keys of the custom properties that belong to the custom object.\n",
                  "example": {
                    "employee_id": "9876"
                  },
                  "type": "object",
                  "x-struct": null,
                  "x-validate": null
                }
              },
              "type": "object",
              "x-struct": null,
              "x-validate": null
            },
            "maxItems": 4000,
            "minItems": 1,
            "type": "array",
            "x-struct": null,
            "x-validate": null
          },
          "custom_data": {
            "description": "An object containing the ID of the custom object with which the contact should be associated.\nThe resource should always be `contact`.\n",
            "example": {
              "custom_object_id": "ad4dd52e-127e-4173-a7be-c729ac66f5b8",
              "resource": "contact"
            },
            "properties": {
              "custom_object_id": {
                "description": "The ID of the custom object with which the contact should be associated.",
                "example": "ad4dd52e-127e-4173-a7be-c729ac66f5b8",
                "format": "uuid",
                "type": "string",
                "x-struct": null,
                "x-validate": null
              },
              "resource": {
                "description": "The resource with which the custom object should be associated.",
                "example": "contact",
                "type": "string",
                "x-struct": null,
                "x-validate": null
              }
            },
            "type": "object",
            "x-struct": null,
            "x-validate": null
          },
          "opt_in_to": {
            "description": "Setting to opt in contacts from contacts param to specific channels",
            "items": {
              "description": "Channel",
              "properties": {
                "id": {
                  "description": "Channel ID",
                  "example": "7a6ae2be-3493-4e3f-8beb-08b1680d9ddb",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                },
                "phone": {
                  "description": "Channel phone number",
                  "example": "+14155552671",
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                }
              },
              "type": "object",
              "x-struct": null,
              "x-validate": null
            },
            "maxItems": 1000,
            "minItems": 1,
            "type": "array",
            "x-struct": null,
            "x-validate": null
          },
          "opt_in_to_all_channels": {
            "default": true,
            "description": "Setting to opt in contacts from contacts param to all organization channels",
            "type": "boolean",
            "x-struct": null,
            "x-validate": null
          }
        },
        "required": [
          "contacts"
        ],
        "title": "UpsertContactParams",
        "type": "object",
        "x-struct": null,
        "x-validate": null
      },
      "ContactUnprocessableContentErrorResponse": {
        "description": "List of error responses for invalid Contact POST or PUT requests.\n",
        "example": {
          "errors": [
            {
              "description": "Contact name should be at most 255 characters"
            }
          ],
          "status": 422
        },
        "properties": {
          "errors": {
            "description": "Contact 422 errors",
            "items": {
              "description": "Single 422 error",
              "properties": {
                "description": {
                  "enum": [
                    "Contact name should be at most 255 characters",
                    "Contact email should be at most 255 characters",
                    "Email must be 255 characters or less",
                    "Contact email has invalid format",
                    "Contact phone has invalid format",
                    "At least one of Contact phone, email, external_id or meta_id must be present.",
                    "The length of the Opt In To field should not be more than 1000",
                    "Invalid Opt In To channels provided",
                    "Missing Contacts",
                    "The length of the Contacts field should not be more than 4000",
                    "All Contacts phone numbers, email addresses, external IDs and/or default channel IDs are invalid",
                    "Integration ID requires External ID",
                    "contacts integration_id requires external_id",
                    "contacts integration_id must be a valid UUID"
                  ],
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                }
              },
              "type": "object",
              "x-struct": null,
              "x-validate": null
            },
            "type": "array",
            "x-struct": null,
            "x-validate": null
          },
          "status": {
            "enum": [
              422
            ],
            "type": "integer",
            "x-struct": null,
            "x-validate": null
          }
        },
        "title": "ContactUnprocessableContentErrorResponse",
        "type": "object",
        "x-struct": "Elixir.DashboardApiWeb.Public.V1.Documentation.Contacts.Contact.Errors.UnprocessableContent",
        "x-validate": null
      },
      "ContactNotFoundErrorResponse": {
        "description": "Contact Not Found Error Response schema",
        "example": {
          "errors": [
            {
              "description": "Contact not found"
            }
          ],
          "status": 404
        },
        "properties": {
          "errors": {
            "description": "Contact 404 error",
            "items": {
              "properties": {
                "description": {
                  "enum": [
                    "Contact not found"
                  ],
                  "type": "string",
                  "x-struct": null,
                  "x-validate": null
                }
              },
              "type": "object",
              "x-struct": null,
              "x-validate": null
            },
            "type": "array",
            "x-struct": null,
            "x-validate": null
          },
          "status": {
            "enum": [
              404
            ],
            "type": "integer",
            "x-struct": null,
            "x-validate": null
          }
        },
        "title": "ContactNotFoundErrorResponse",
        "type": "object",
        "x-struct": "Elixir.DashboardApiWeb.Public.V1.Documentation.Contacts.Contact.Errors.NotFound",
        "x-validate": null
      },
      "UpsertContactResponse": {
        "description": "Response schema for upserting contacts.",
        "example": {
          "data": {
            "message": "Contacts are being imported",
            "result": {
              "import_id": "d4a11f39-8c22-4935-ba23-30be39aedc0f",
              "status": "pending",
              "target": "bulk_opt_in"
            }
          }
        },
        "properties": {
          "data": {
            "description": "Data for the upserted contacts.",
            "properties": {
              "message": {
                "description": "The message of the import",
                "example": "Contacts are being imported",
                "type": "string",
                "x-struct": null,
                "x-validate": null
              },
              "result": {
                "properties": {
                  "import_id": {
                    "description": "The ID of the import",
                    "example": "e1cad7a3-50a1-4695-96d9-80f3aede440b",
                    "type": "string",
                    "x-struct": null,
                    "x-validate": null
                  },
                  "status": {
                    "description": "The status of the import",
                    "example": "pending",
                    "type": "string",
                    "x-struct": null,
                    "x-validate": null
                  },
                  "target": {
                    "description": "The target of the import",
                    "example": "bulk_opt_in",
                    "type": "string",
                    "x-struct": null,
                    "x-validate": null
                  }
                },
                "type": "object",
                "x-struct": null,
                "x-validate": null
              }
            },
            "type": "object",
            "x-struct": null,
            "x-validate": null
          }
        },
        "title": "UpsertContactResponse",
        "type": "object",
        "x-struct": null,
        "x-validate": null
      }
    },
    "securitySchemes": {
      "authorization": {
        "in": "header",
        "name": "X-WHIPPY-KEY",
        "type": "apiKey"
      },
      "bearer": {
        "description": "OAuth Access token",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Whippy Public API.\n\n## Authentication\n\nThe Whippy Public API supports two authentication methods:\n\n**API Key** — Generate an organization API token and send it with every request as the `X-WHIPPY-KEY` header.\n\n**OAuth Bearer Token** — Obtain an access token via the OAuth authorization flow and send it as a Bearer token in the `Authorization` header (e.g. `Authorization: Bearer <access_token>`).\n\n## Rate Limiting\n\nThe Whippy API is rate limited. Different endpoints have different rate limits.\nEach request to the API will return the following headers, which can be used to keep track of your rate limit status:\n * `x-ratelimit-max`: The maximum number of requests that the consumer is permitted to make in the given time period.\n * `x-ratelimit-remaining`: The number of requests remaining in the current rate limit window.\n * `x-ratelimit-interval-milliseconds`: The rate limit window in milliseconds.\n * `x-ratelimit-remaining-interval-milliseconds`: The time in milliseconds until the rate limit window resets.\n",
    "title": "Whippy Public API",
    "version": "1.0"
  },
  "openapi": "3.0.0",
  "paths": {
    "/v1/contacts/upsert": {
      "post": {
        "callbacks": {},
        "deprecated": false,
        "description": "Bulk creates or updates contacts in an organization.\n\nOptionally, it can opt in contacts to all organization channels or specific channels.\nAlso, the contacts in the list can optionally be associated with a custom object by providing the custom object ID\nin the top level of the request body and the properties of the custom object in the `properties` key of each contact.\nА contact's relationship with a custom object is always one-to-one.\n\n**Important: The following fields of a contact are unique and also nullable**:\n  - `external_id`\n  - `phone`\n  - `email`\n\nThis means that if a contact with the same value for any of these fields already exists in the organization,\nthe contact will be updated instead of created.\nThe following update strategy is applied:\n- If a contact is present in the payload where the `external_id` conflicts with the `external_id` of an existing contact:\n  - If the existing contact has a different `email` and `phone` than the new contact, the existing contact will be updated with the new data and the email and phone will be overwritten, as long as no other contact has the same email or phone.\n- If a contact is present in the payload where the `phone` conflicts with the `phone` of an existing contact:\n  - If the existing contact has the same `external_id` as the new contact or its value is not set, the existing contact will be updated with the new data.\n  - If the existing contact has a different `external_id` than the new contact, a new contact without a phone will be created.\n- If a contact is present in the payload where the `email` conflicts with the `email` of an existing contact:\n  - If the existing contact has the same `external_id` and `phone` as the new contact or their values are not set, the existing contact will be updated with the new data.\n  - If the existing contact has a different `external_id` and `phone` than the new contact, a new contact without an email will be created.\n",
        "operationId": "upsertContacts",
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertContactParams"
              }
            }
          },
          "description": "Contact Upsert",
          "required": false
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertContactResponse"
                }
              }
            },
            "description": "Contact Upsert Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotAuthenticatedErrorResponse"
                }
              }
            },
            "description": "Not Authenticated Response"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactNotFoundErrorResponse"
                }
              }
            },
            "description": "Not Found"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactUnprocessableContentErrorResponse"
                }
              }
            },
            "description": "Unprocessable Content Response"
          }
        },
        "security": [
          {},
          {
            "authorization": []
          },
          {
            "bearer": []
          }
        ],
        "summary": "Upsert Contacts",
        "tags": [
          "Contact"
        ]
      }
    }
  },
  "security": [],
  "servers": [
    {
      "url": "https://api.whippy.co",
      "variables": {}
    }
  ]
}
```