---
title: "Import or update contacts in bulk"
method: POST
path: "/api/v1/sites/{id}/contacts"
tags: ["CRM"]
---

# Import or update contacts in bulk

`POST /api/v1/sites/{id}/contacts`

## Request

- Base URL: `https://app.mondovo.com`
- URL: `https://app.mondovo.com/api/v1/sites/{id}/contacts`
- Auth: HTTP bearer

Create-or-update up to 500 contacts in one call — the endpoint for importing a customer list. Each contact is matched against the site’s existing contacts on `match_on` (`email` by default, case-insensitively): a match is updated, everything else is created, so re-sending the same batch is safe. Any attribute the CRM has no column for goes in `custom_fields` as flat key/value pairs, and is auto-declared as a filterable field unless `create_missing_fields` is false. Tags merge with the contact’s existing tags by default; `tags_mode: "replace"` overwrites them. Custom fields always merge key-by-key. Use `contact_fields_mode: "fill_missing"` to enrich contacts while preserving existing native contact values; retry a 409 when a concurrent edit wins. Send `dry_run: true` first to see the created/updated counts without writing. Two deliberate limits: `status` (pipeline stage) applies to NEW contacts only — an import never moves an existing contact’s stage — and no contact is ever deleted. Requires scope `crm:write`.

## Request body

- UpsertContactsInput
  - `contacts` object[], required
    - `email` string, email
    - `phone` string
    - `first_name` string
    - `last_name` string
    - `company` string
    - `job_title` string
    - `address_line1` string
    - `address_line2` string
    - `city` string
    - `state` string
    - `postal_code` string
    - `country` string
    - `notes` string
    - `status` string: Pipeline stage key. Applied to NEW contacts only.
    - `source` string: Defaults to `api_import`.
    - `acquisition_source` string
    - `acquisition_campaign` string
    - `tags` string[]
    - `custom_fields` object: Flat key/value attributes — scalars only. Keys must be identifier-shaped (letter or underscore, then letters/digits/underscores, max 64 chars) because they are used as filter expressions. Nested objects are rejected: every filter and segment reads these as text, so a nested value would store but never be visible.
    - `assigned_to` string, uuid
    - `last_contacted_at` string, date-time
    - `next_follow_up_at` string, date-time
  - `match_on` 'email' | 'phone': Which field identifies an existing contact. Matching is scoped to this site, and email matching is case-insensitive.
  - `tags_mode` 'merge' | 'replace': Merge unions incoming tags with the ones already on the contact. Replace overwrites them — which discards any tag applied in the app since the last import.
  - `contact_fields_mode` 'overwrite' | 'fill_missing': fill_missing preserves populated native contact fields and fills blank phone/name fields atomically. Tags and custom fields still merge. Concurrent edits can return a retryable 409.
  - `create_missing_fields` boolean: Auto-declare a text field for each unrecognised `custom_fields` key so it is immediately filterable. Values are stored either way.
  - `dry_run` boolean: Validate and report what would change without writing.

## Response `200`

The batch was applied (or evaluated, for a dry run).

- object
  - `site_id` string, uuid
  - `created` integer
  - `updated` integer
  - `total_submitted` integer
  - `duplicates_in_batch` integer: Rows collapsed because they shared a match key.
  - `stage_changes_ignored` integer: `status` values skipped because the contact already existed.
  - `fields_defined` string[]: Custom fields auto-declared by this call.
  - `matched_on` 'email' | 'phone'
  - `message` string

## Other responses

- `400`: Error codes: `validation_error`, `missing_match_key`, `unknown_stage` (response carries `valid_stages`).
- `401`: Authentication failed. Error codes: `invalid_api_key` (missing/unknown key), `key_revoked`, `key_expired`.
- `403`: Authenticated but not allowed. Error codes: `addon_required`, `missing_scope`, and `demo_site_readonly` (contacts on a demo site cannot be modified).
- `404`: `site_not_found`.
- `409`: `contact_conflict` — a concurrent write created one of these contacts and the retry still conflicted. The batch is idempotent: send it again.
- `429`: Rate limit exceeded (`rate_limit_exceeded`). The body includes `retry_after_seconds`, `limit`, and `window` ("minute" or "hour"); a `Retry-After` header is also set.
- `503`: `stages_unreadable` — pipeline stages could not be read, so `status` could not be validated. Retry, or omit `status`.

---

[API](https://skmtc.dev/mondovo/apis/mondovo-public-api.md) · [All operations](https://skmtc.dev/mondovo/apis/mondovo-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mondovo/apis/mondovo-public-api/revisions/1baa3e2865ba?raw)
