# Updating a project

## Updating a project
`PATCH /projects/{project_id}` writes back the fields your ERP owns.
The canonical use is reconciling identifiers: your ERP creates a
project / commission number and PATCHes it into `object_number` so
every later `tender.completed` / `order.completed` webhook and
`GET /projects` lines up with your own key.
Writable fields (all optional): `object_number`, `name`, `status`,
`estimated_value`, `currency`, `submission_date`, `custom_fields`.
If `object_number` is the only field you need to write, you can skip
PATCH entirely and send it as the `external_id` of an acknowledgement
instead — see *Acknowledging a project*.
Semantics:
- **Partial.** Send only what you change. An omitted field is left
untouched; an explicit `null` clears that field. `status` is the one
exception — it cannot be `null` (a project always has a lifecycle
state), and an unknown status value is a `400`.
- **`object_number` is a grouping key.** Mercura derives the
master-project grouping from this value at read time, so writing it
simply moves the one project row you addressed into (or out of) a
group — sibling rows that shared the old value are not rewritten.
- **`custom_fields` is label-keyed and shallow-merged.** Use the same
labels you receive on `GET` (and, for a multi-select, the same option
labels). Keys you include are set; a key mapped to `null` clears that
one field; keys you omit are left as they are. An unknown label — or
an unknown option label on a multi-select — returns
`400 VALIDATION_FAILED` naming the offending keys, so a typo fails
loudly instead of silently writing nothing.

Optimistic concurrency: read the project first, then echo the `ETag`
you received as an `If-Match` header on the PATCH. If someone else
changed the project in the meantime the ETag no longer matches and the
request is rejected with `412 Precondition Failed` — re-fetch and
retry. Omit `If-Match` (or send `If-Match: *`) to skip the check. A
no-op edit (an empty body, or values equal to the current ones) does
not advance the project's `updated_at`.

```http
PATCH /projects/90 HTTP/1.1
Authorization: Bearer <your-api-key>
If-Match: "3f8b6d21..."
Content-Type: application/json

{
  "object_number": "P-2026-00417",
  "status": "CUSTOMER_WON_AWARDED_TO_US",
  "custom_fields": { "ERP-Auftragsnummer": "SO-88231" }
}
```

Version: 1.79.2

## Description:

  - `Updating a project` ()
    `PATCH /projects/{project_id}` writes back the fields your ERP owns.
The canonical use is reconciling identifiers: your ERP creates a
project / commission number and PATCHes it into `object_number` so
every later `tender.completed` / `order.completed` webhook and
`GET /projects` lines up with your own key. Writable fields (all optional): `object_number`, `name`, `status`,
`estimated_value`, `currency`, `submission_date`, `custom_fields`. If `object_number` is the only field you need to write, you can skip
PATCH entirely and send it as the `external_id` of an acknowledgement
instead — see *Acknowledging a project*. Semantics: - **Partial.** Send only what you change. An omitted field is left
untouched; an explicit `null` clears that field. `status` is the one
exception — it cannot be `null` (a project always has a lifecycle
state), and an unknown status value is a `400`.
- **`object_number` is a grouping key.** Mercura derives the
master-project grouping from this value at read time, so writing it
simply moves the one project row you addressed into (or out of) a
group — sibling rows that shared the old value are not rewritten.
- **`custom_fields` is label-keyed and shallow-merged.** Use the same
labels you receive on `GET` (and, for a multi-select, the same option
labels). Keys you include are set; a key mapped to `null` clears that
one field; keys you omit are left as they are. An unknown label — or
an unknown option label on a multi-select — returns
`400 VALIDATION_FAILED` naming the offending keys, so a typo fails
loudly instead of silently writing nothing. Optimistic concurrency: read the project first, then echo the `ETag`
you received as an `If-Match` header on the PATCH. If someone else
changed the project in the meantime the ETag no longer matches and the
request is rejected with `412 Precondition Failed` — re-fetch and
retry. Omit `If-Match` (or send `If-Match: *`) to skip the check. A
no-op edit (an empty body, or values equal to the current ones) does
not advance the project's `updated_at`. ```http
PATCH /projects/90 HTTP/1.1
Authorization: Bearer <your-api-key>
If-Match: "3f8b6d21..."
Content-Type: application/json

{
  "object_number": "P-2026-00417",
  "status": "CUSTOMER_WON_AWARDED_TO_US",
  "custom_fields": { "ERP-Auftragsnummer": "SO-88231" }
}
```

