# Public Projects Update

Edit one project in place and read back the re-projected result.
Send only the fields you want to change; omitted fields are left untouched,
and an explicit `null` clears a field (except `status`, which cannot be
null). `object_number` writes the ERP-facing "Objektnummer"; `status`
takes the UPPERCASE `PublicProjectStatus` values; `custom_fields` is
label-keyed and shallow-merged (a key set to `null` clears that field).
Pass the GET's ETag as `If-Match` to be rejected with `412` if the
project changed since you read it.

Endpoint: PATCH /projects/{project_id}
Version: 1.79.2

## Path parameters:

  - `project_id` (string, required)

## Header parameters:

  - `If-Match` (string | null)
    Optional strong ETag from a prior GET; a mismatch is a 412 (someone else edited the project).

  - `authorization` (string | null)
    Bearer <mrc_live_…> API key

## Request fields (application/json):

  - `object_number` (string | null)
    ERP-facing grouping key (Objektnummer). `null` clears it.

  - `name` (string | null)
    Display name. `null` clears it.

  - `status` (string)
    Lifecycle status of a Mercura project.
Translated from the internal `ProjectStatus` enum — the internal
values are lower-cased for historical reasons; the public API uses
UPPERCASE (matches `PublicOfferStatus` and the platform-wide
enum convention in `CLAUDE.md`). New values are SemVer MINOR
(additive); removed values are MAJOR. Translation table lives in
`projects.service._STATUS_MAP` so a missing case is a test
failure, not silent passthrough.
    Enum: "ACTIVE", "PROCESSED", "BID_SUBMITTED", "CUSTOMER_LOST", "CUSTOMER_WON_PENDING", "CUSTOMER_WON_AWARDED_ELSEWHERE", "CUSTOMER_WON_AWARDED_TO_US"

  - `estimated_value` (number | null)

  - `currency` (string | null)

  - `submission_date` (string | null)
    Free-form submission deadline (ISO date or free text). `null` clears it.

  - `custom_fields` (object | null)
    Label-keyed custom-field values to shallow-merge. Keys are the organisation's custom-column labels; `multi_select` values are option labels. A `null` value clears that field. Unknown labels or option labels are a 400.

## Response 200:

  - `200` (unknown)
    Successful Response

## Response 200 fields (application/json):

  - `id` (string, required)
    Public id; equals the internal Project.id as string.

  - `object_number` (string | null)
    ERP-facing grouping key (Objektnummer). Multiple projects can share an `object_number` — filter on `GET /projects?object_number=...` returns all matching rows. Writable via `PATCH /projects/{project_id}` or by supplying `external_id` on `POST /projects/{project_id}/acknowledgements`.

  - `name` (string | null)

  - `status` (string, required)
    Lifecycle status of a Mercura project.
Translated from the internal `ProjectStatus` enum — the internal
values are lower-cased for historical reasons; the public API uses
UPPERCASE (matches `PublicOfferStatus` and the platform-wide
enum convention in `CLAUDE.md`). New values are SemVer MINOR
(additive); removed values are MAJOR. Translation table lives in
`projects.service._STATUS_MAP` so a missing case is a test
failure, not silent passthrough.
    Enum: "ACTIVE", "PROCESSED", "BID_SUBMITTED", "CUSTOMER_LOST", "CUSTOMER_WON_PENDING", "CUSTOMER_WON_AWARDED_ELSEWHERE", "CUSTOMER_WON_AWARDED_TO_US"

  - `estimated_value` (number | null)

  - `currency` (string | null)

  - `submission_date` (string | null)
    Free-form submission deadline as originally supplied (ISO date or free text).

  - `responsible_user` (object)
    The Mercura user assigned to a project.
Small denormalised bundle rather than a bare id — partners rendering
an offer inbox rarely need to hit a second endpoint just to display
"Assigned to Anna Schmidt". Email is included so downstream systems
can key on it (Mercura's `supabase_id` is not portable).

  - `responsible_user.id` (string, required)
    Mercura user id (opaque; keep or drop on the partner side).

  - `responsible_user.name` (string, required)

  - `responsible_user.email` (string | null)

  - `construction_site_address` (object)
    Postal address shared across customers and suppliers.
    Example: {"city":"Düsseldorf","country":"DE","is_default":true,"kind":"billing","postal_code":"40210","region":"NRW","street":"Industriestraße","street_number":"12"}

  - `construction_site_address.kind` (string | null)
    Address type, e.g. billing | shipping | hq

  - `construction_site_address.street` (string | null)

  - `construction_site_address.street_number` (string | null)
    House / building number kept separate from street for ERPs that store them apart

  - `construction_site_address.postal_code` (string | null)

  - `construction_site_address.city` (string | null)

  - `construction_site_address.region` (string | null)

  - `construction_site_address.country` (string | null)
    ISO 3166-1 alpha-2 country code

  - `construction_site_address.is_default` (boolean | null)

  - `custom_fields` (object | null)
    Organisation-defined custom-field values. Keys and value shapes are governed by the organisation's `custom_column_definitions`.

  - `request_ids` (array)
    Request ids linked to this project via `Request.project_id`. Round-trip against `GET /offers/{id}` to fetch per-request content.

  - `acknowledgement` (object)
    The partner's last reported ERP/CRM import outcome for a project.
Used as the `acknowledgement` field on `PublicProjectOut` and as the
response body of `POST /projects/{project_id}/acknowledgements`.
`status` / `message` / `metadata` reflect the most recent
acknowledgement; `external_id` is kept once provided
(last-provided-wins). Like the tender acknowledgement it is mirrored onto a
first-class field: a supplied `external_id` is also written to the
project's `object_number` (the Objektnummer), which is what Mercura sends
back out as `erp_object_id` on later tender/order exports.
    Example: {"acknowledged_at":"2026-08-20T09:15:00Z","external_id":"4500012345","metadata":{"erp_client":"100"},"status":"SUCCESS"}

  - `acknowledgement.status` (string, required)
    Outcome a partner reports after importing a project into their ERP/CRM.
Translated from the internal `RequestAckStatus` (the identical axis the
tender/order acknowledgements use) via
`projects.service._PROJECT_ACK_STATUS_MAP`. A *separate axis* from
`PublicProjectStatus`: that is Mercura-side lifecycle, this is the
partner's import result.
    Enum: "SUCCESS", "FAILED"

  - `acknowledgement.external_id` (string | null)
    Partner-side ERP/CRM id of the created project / record (e.g. its ERP document number).

  - `acknowledgement.message` (string | null)
    Free-text note about the outcome. On `FAILED` this is the failure reason (required); on `SUCCESS` an optional note.

  - `acknowledgement.metadata` (object | null)
    Optional extra references / raw payload the partner echoed back for traceability.

  - `acknowledgement.acknowledged_at` (string, required)
    When the partner reported this outcome (server receipt time).

  - `created_at` (string, required)

  - `updated_at` (string, required)

## Response 400:

  - `400` (unknown)
    Validation failed (unknown custom-field label / option, null status, …)

## Response 404:

  - `404` (unknown)
    Project not found

## Response 412:

  - `412` (unknown)
    If-Match ETag does not match the current project (it changed since your GET)

## Response 422:

  - `422` (unknown)
    Validation Error

## Response 422 fields (application/json):

  - `detail` (array)

  - `detail.loc` (array, required)

  - `detail.msg` (string, required)

  - `detail.type` (string, required)

  - `detail.input` (any)

  - `detail.ctx` (object)

