> For the complete documentation index, see [llms.txt](https://developer.parvenu.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.parvenu.io/manage-lists/update-contacts-in-bulk.md).

# Update Contacts in Bulk

Change or clear fields on up to 5,000 contacts in a list in one call.

<mark style="color:green;">`POST`</mark> `/update_contacts_bulk`

Applies many updates to contacts in `listname` in one call. Each entry carries its own `match`, `fields` and `clear_fields`, with the same matching and the same write rules as [Update a Contact](/manage-lists/update-a-contact.md).

{% hint style="info" %}
**Free.** `credits_used` is always `0`.
{% endhint %}

{% hint style="info" %}
**Synchronous, not a background job.** Unlike the bulk enrichment endpoints, this returns the result immediately. There is no `job_id`, and you do not need to call Check List Status or Download List Data afterwards.
{% endhint %}

{% hint style="warning" %}
**Send `dry_run: true` first.** Every contact matching an entry is updated, and a bare `organization_name` or `domain` reaches everyone at that company. The dry run reports how many would change and writes nothing.
{% endhint %}

{% hint style="danger" %}
**There is no undo.** The old values are replaced.
{% endhint %}

## Headers

| Name         | Value              |
| ------------ | ------------------ |
| Content-Type | `application/json` |

## Body

| Name       | Type             | Description                                                                 |
| ---------- | ---------------- | --------------------------------------------------------------------------- |
| `api_key`  | string           | **Required.** Your API key.                                                 |
| `listname` | string           | **Required.** The list the contacts are in.                                 |
| `contacts` | array of objects | **Required.** One entry per update, each `{match, fields, clear_fields}`.   |
| `dry_run`  | boolean          | Default `false`. When `true`, reports what would change and writes nothing. |

Each entry's `match`, `fields` and `clear_fields` work exactly as on [Update a Contact](/manage-lists/update-a-contact.md): `null` and `""` values are skipped, `clear_fields` empties, and a field both set and cleared is refused. Each entry can also carry `custom_fields` and `clear_custom_fields` for the list's [custom columns](/manage-lists/custom-columns.md).

## Rules

* **All or nothing on field names.** One unknown field name anywhere in the call refuses the whole call, so a typo cannot half-apply a batch.
* **Later entries win.** When two entries reach the same contact, they are applied in order, field by field. A field only the first entry sets keeps its value; a field both set ends up with the second entry's value.
* **Up to 5,000 entries** in one call. Past that, the call is refused rather than quietly truncated — send them in batches.
* **Send very large updates in batches.** Use `dry_run: true` to see how many contacts a call would change, and keep each call to about 1,000 changed contacts. A call that changes 1,200 contacts typically answers in a few seconds.

## Slow calls (504)

If a call takes longer than 75 seconds, it answers `504` with the message *"The update is taking longer than usual and may still finish. Check the list, or run the same call with dry\_run: true, before retrying."*

A `504` does **not** mean the update failed. The work keeps going and may complete after the answer. Before sending it again, check the list, or repeat the call with `dry_run: true`. A `500` "Could not connect to the list service" means the call never reached the list and nothing changed.

## Response

The same fields as [Update a Contact](/manage-lists/update-a-contact.md), totalled across the whole call.

| Field                 | Description                                                 |
| --------------------- | ----------------------------------------------------------- |
| `status`              | `success`, or `not_found` when nothing in the list matched. |
| `data.list_name`      | The list.                                                   |
| `data.rows_matched`   | How many contacts matched.                                  |
| `data.rows_updated`   | How many were changed.                                      |
| `data.fields_updated` | Which fields were written, across all entries.              |
| `data.fields_cleared` | Which fields were emptied, across all entries.              |
| `data.matched_on`     | Which identifier fields were used across the call.          |
| `data.dry_run`        | Whether this was a dry run.                                 |
| `credits_used`        | Always `0`.                                                 |

## Example

{% tabs %}
{% tab title="Input" %}

```json
{
  "api_key": "your_api_key",
  "listname": "Q4 Roofers",
  "contacts": [
    {
      "match": {
        "person_linkedin_url": "linkedin.com/in/samrivera"
      },
      "fields": {
        "current_title": "Head of Operations"
      }
    },
    {
      "match": {
        "domain": "summitroofing.com"
      },
      "fields": {
        "company_linkedin_url": "https://www.linkedin.com/company/summit-roofing",
        "headcount": 42,
        "company_tech_stack": ["HubSpot", "QuickBooks"]
      }
    }
  ]
}
```

{% endtab %}

{% tab title="Output" %}

```json
{
  "status": "success",
  "credits_used": 0.0,
  "credits_remaining": 6042.2,
  "message": "Updated 2 contacts in 'Q4 Roofers'.",
  "data": {
    "list_name": "Q4 Roofers",
    "rows_matched": 2,
    "rows_updated": 2,
    "fields_updated": [
      "company_linkedin_url",
      "company_size",
      "company_tech_stack",
      "current_title",
      "headcount"
    ],
    "fields_cleared": [],
    "matched_on": ["person_linkedin_url", "domain"],
    "dry_run": false
  }
}
```

{% endtab %}
{% endtabs %}

The LinkedIn URL matched without `https://www.`. `company_size` appears in `fields_updated` although it was not sent: an exact `headcount` fills in the matching size band.

## No match

A call that matches nothing answers `status: not_found` with `rows_matched: 0` and the message `No contacts in this list matched.` Nothing changed and nothing was charged.
