- Public Tenders Update
Edit one tender in place and read back the re-projected result.
Two addressing modes: a sparse top-level positions[] (recommended — edit or delete existing lines by id) and the nested chapters tree (required for adds; also accepts edits). Edit a line by id; delete one with {"id": "…", "op": "delete"} (soft-delete); add a line by sending it without an id (article_number for an article line — with optional price/discount — else a free-text line under a chapter). Omitted lines are left untouched. totals.net_total / gross_total persist as the partner's authoritative amounts. Pass the GET's ETag as If-Match to be rejected with 412 if the tender changed since you read it.
Closing a tender. Send {"status": "CANCELLED"} for a tender that was resolved outside Mercura — its CRM case was closed, or it was quoted by hand — so it leaves the open inbox instead of accumulating there. It is the only writable transition; echoing the tender's current status is a no-op, so a full GET body still round-trips.
Optional summary-total overrides (net/gross totals, header discount).
{ "additional_discount_percentage": "3.00" }
Sparse edits/deletes addressed by line id (no chapter path). Adds must use chapters, except an article note (parent_line_id), which is accepted here.
[ { "id": "840210", "net_price": "74.90" }, { "article_number": "LEU-1200-30-830", "id": "840215", "net_price": "71.10" }, { "id": "840290", "op": "delete" } ]
Your ERP's id for the offer created from this tender. Set-only: a value is written and kept, null (or omitting the key) leaves the stored id untouched so a full GET body round-trips, and there is no way to clear it — send a new value to replace it. The same field is set by an acknowledgement's external_id; use whichever fits your flow (the acknowledgement additionally records the import outcome).
Label-keyed REQUEST custom-field values to shallow-merge onto the tender's underlying request. Keys are the organisation's custom-column labels; multi_select values are option labels. A null value clears that field; sending the whole object as null (or omitting it) is a no-op, so a full GET body round-trips. Unknown labels or option labels are a 400. Symmetric with the request_custom_fields you read on GET.
Lifecycle status. The only writable transition is to CANCELLED — use it to close a tender that was resolved outside Mercura (e.g. its case was closed in your CRM), so it leaves the open inbox. Sending the tender's current status is a no-op, so a full GET body still round-trips. Any other target is a 400; cancelling a tender that is PARSING or already DONE is a 409.
- Mock serverhttps://docs.mercura.ai/_mock/openapi/tenders/{tender_id}
- https://docs.mercura.ai/api/public/v1https://docs.mercura.ai/api/public/v1/tenders/{tender_id}
- Productionhttps://prod-euapi.mercura.ai/api/public/v1/tenders/{tender_id}
curl -i -X PATCH \
'https://docs.mercura.ai/_mock/openapi/tenders/{tender_id}' \
-H 'Content-Type: application/json' \
-H 'If-Match: string' \
-H 'authorization: string' \
-d '{
"erp_offer_id": "SAP-4500012345",
"positions": [
{
"id": "840210",
"net_price": "74.90"
},
{
"article_number": "LEU-1200-30-830",
"id": "840215",
"net_price": "71.10"
},
{
"id": "840290",
"op": "delete"
}
],
"totals": {
"additional_discount_percentage": "3.00"
}
}'Successful Response
Lifecycle state of a tender (open enum — new values are SemVer MINOR).
A translated subset of the internal RequestStatus. The mapping lives in tenders.service._TENDER_STATUS_MAP and a test asserts it covers every internal value.
When the tender was last completed (offer exported / sent). Refreshed on every completion; null if never completed. Use it with the GET /tenders?completed_since=… completion feed.
Customer reference only — resolve full details via GET /customers/{customer_id}. Null when the tender has no customer.
Indirect-customer reference (Mercura's indirekter Kunde) — resolve full details via GET /customers/{customer_id}. Distinct from customer_id (the direct customer / Sold-To). Null when the tender has no indirect customer.
Project reference only — resolve full details via GET /projects/{project_id}. Null when the tender has no project.
The project's display name, surfaced inline for convenience (same value as name on GET /projects/{project_id}). Null when the tender has no project. Read-only.
Bid-submission date (Submissions-/Abgabetermin) as originally supplied on the project — an ISO date or free text; null when unknown. This is the tender's submission deadline, not an offer-validity ("gültig bis") date.
Mercura's internal handling deadline for this request (typically the submission date minus a buffer) — an ISO date or free text; null when unset. Informational only; not the submission deadline and not an offer-validity date.
Email of the Mercura user responsible for this tender (its Bearbeiter / Sachbearbeiter) — not necessarily the user who triggered the export. Null when no responsible user is assigned.
The primary Mercura branch (Niederlassung) assigned to this tender ({id, name}), or null when none is assigned. Read-only.
{ "id": "7", "name": "Niederlassung München" }
Organisation-defined custom fields on the request, keyed by display label. Null when none.
{ "Angebots-Nr": "AN-2026-0042" }
Organisation-defined custom fields on the responsible Mercura user (the user_email above), keyed by display label — e.g. a user's SGI. Null when the tender has no responsible user or the user has no custom fields.
{ "SGI": "12345" }
[ { "chapter_number": "01", "chapters": [ … ], "id": "5502", "is_pretext": false, "name": "Elektroinstallation", "positions": [] } ]
Aggregate amounts for the tender, all in EUR.
positions_subtotal is a derived helper: the sum of line_net across lines with is_counted_in_total = true (i.e. excluding alternatives and Bedarfspositionen ohne Gesamtbetrag / OPTIONAL_WITHOUT_TOTAL). net_total / gross_total are the authoritative request totals — the same figures Mercura prints on the PDF — preferring the request-level persisted values (which a partner can override via PATCH — see PublicTenderTotalsUpdate) and otherwise falling back to the line sums.
The two can legitimately differ: net_total is after the header additional_discount_percentage (positions_subtotal is before it), and Mercura's persisted total may include optional positions or other adjustments that the counted-line subtotal omits. Treat net_total as the source of truth for the amount owed and positions_subtotal as an informational sum.
{ "additional_discount_percentage": "3.00", "gross_total": "5992.80", "net_total": "4941.41", "positions_subtotal": "5094.24" }
The partner-side ERP offer id, when known — from your acknowledgement's external_id or a previous Mercura-managed export; otherwise null.
Your last reported ERP/CRM import outcome for this tender, or null if none yet.
{ "acknowledged_at": "2026-07-09T12:00:12Z", "external_id": "AB-2026-5567", "message": "Angebot in ERP angelegt.", "metadata": { "erp_document": "AB-2026-5567", "warehouse": "Z-100" }, "status": "SUCCESS" }
{ "acknowledgement": { "acknowledged_at": "2026-07-09T12:00:12Z", "external_id": "AB-2026-5567", "message": "Angebot in ERP angelegt.", "metadata": { … }, "status": "SUCCESS" }, "branch": { "id": "7", "name": "Niederlassung München" }, "chapters": [ { … } ], "completed_at": "2026-07-09T11:59:58Z", "created_at": "2026-07-08T09:14:00Z", "customer_id": "K-10042", "deadline": "2026-08-13", "erp_offer_id": "AB-2026-5567", "id": "12345", "indirect_customer_id": "K-20099", "project_id": "90", "project_name": "Neubau Bürogebäude Nord", "request_custom_fields": { "Angebots-Nr": "AN-2026-0042" }, "request_id": "12345", "status": "DONE", "submission_date": "2026-08-20", "totals": { "additional_discount_percentage": "3.00", "gross_total": "5992.80", "net_total": "4941.41", "positions_subtotal": "5094.24" }, "updated_at": "2026-07-09T12:00:12Z", "user_custom_fields": { "SGI": "12345" }, "user_email": "anna.schmidt@example.com" }