# Editing a tender

## Editing a tender
`PATCH /tenders/{tender_id}` is a **partial, in-place** edit that returns the
re-projected tender.
**Recommended: send only what changed (sparse).** Put the lines you touched in a
top-level `positions[]`, each addressed by its `id` — no chapter path needed
(line ids are unique within a tender). This is the primary contract for keeping
an ERP in sync and the one that scales: the body is proportional to your edit,
not to the tender.

```
PATCH /tenders/63365
{ "positions": [ { "id": "10", "net_price": "1.00" }, { "id": "11", "op": "delete" } ] }
```
**Also supported: full-document round-trip.** The body is field-aligned with the
GET response, so you *can* GET a tender, change fields in the nested `chapters[]`
tree, and send it all back — read-only fields are tolerated and ignored, unknown
keys are a `400`. Prefer sparse for large tenders: a full round-trip re-sends and
re-resolves every line. (A discounted line's GET carries `list_price` +
`discount_percentage` + `net_price` together; that's accepted as long as they're
consistent — but if you *change* a price, send only the field you changed, or a
contradictory pair is a `400`.)
- **Edit a line** — top-level `positions[]` (by `id`), or under its chapter.
Price resolution: `list_price` (Brutto) is the anchor, then **at most one** of
`discount_percentage` / `net_price` resolves the other:
  - `discount_percentage` is a **manual** discount applied *on top of* any
catalogue/pricing-rule discount already on the line — i.e. on the
net-of-catalogue base, exactly as the Mercura UI applies it — not a raw
percentage off `list_price`. Editing it recomputes `net_price` off that base
(a catalogue discount is preserved).
  - `net_price` sets the exact per-unit net; the resulting `discount_percentage`
is back-computed from it. **To pin an exact net, send `net_price`.**
Also editable: `quantity`, `unit`, `additional_text`. `description` is editable
only on a free-text line — on an ARTICLE line it comes from the catalogue and
is read-only (sent values are ignored, so a GET body still round-trips).
Note on `additional_text`: it is stored and returned, and some ERP
integrations consume it as their long-text element, but **no Mercura-side
output reads it** — neither the offer PDF nor the GAEB export. To put text on a
line that reaches those, use an **article note** (below).
- **Annotate a line (article note)** — send `parent_line_id` (the `id` of the
line to annotate) plus `description`, and **no** `id`. The note becomes a text
line on the parent's own position — no new position, no own position number —
and its text follows the path an operator-typed note takes: into Mercura's
offer PDF and GAEB export, and folded into the parent line's remark
(`Bemerkung`) in the ERP payload.
This is the one add that needs no chapter path (the parent line already names
its position), so it is accepted in the top-level `positions[]` too. It is an
**upsert**: sending a note for a parent that already has one *replaces* that
note, so a retried PATCH cannot pile up duplicates. A note is text only —
`article_number`, prices, `quantity`, `unit` and `additional_text` on a note
are rejected with a `400` rather than silently dropped, and a note never
enters the totals. To remove one, address the note line itself with
`{ "id": "…", "op": "delete" }`.
- **Delete a line** — `{ "id": "…", "op": "delete" }` (soft-delete). Works in the
top-level `positions[]` or under a chapter.
- **Add a line** — include a position **without** an `id` under an existing
chapter (adds need the chapter target, so they can't go in top-level
`positions[]` — an article note is the exception). With an `article_number` it becomes an article line (resolved
against your catalogue — an unknown number is a `400`); without one, send
`description` for a free-text line. Either way you can set the line's price and
discount on the add (`list_price` + one of `discount_percentage` / `net_price`).
- **Summary totals** — `totals.net_total` / `totals.gross_total` persist as your
authoritative amounts and **win over** Mercura's recalculation (they may
legitimately differ from the line sums — freight, rounding, surcharges).
`totals.additional_discount_percentage` sets the header discount.
- **Request custom fields** — send a label-keyed `request_custom_fields` object to
shallow-merge values onto the tender's underlying request (symmetric with the
read side — send back exactly what you GET). A `null` **value** clears that field;
a whole-object `null` (or omitting it) is a no-op, so a full GET body round-trips.
An unknown label or an unknown `multi_select` option label is a `400`. Use this to
report partner-maintained values back with the prices — e.g. an "Offer Validity"
(Angebotsgültigkeit) custom field maintained in your ERP.
- **Close a tender** — see *Closing a tender* below.
- **Concurrency** — pass the GET's `ETag` as `If-Match`; if the tender changed
since you read it you get a `412` (re-fetch and retry). Omitting `If-Match`
skips the check (last-write-wins).

Apart from an explicit `status` write, editing does not change the tender's
lifecycle `status` and never re-triggers an export.

Version: 1.33.0
