A Project is the real-world construction project (or business opportunity) an incoming request belongs to. Multiple requests — a tender, its follow-up amendments, later related orders — usually roll up under one project. Every incoming LV becomes a tender that Mercura groups into a Project on your behalf, and the tender.completed / order.completed webhooks carry the project_id so partners can reconcile the exported result against the same project record on their side.
Each project carries:
id— the numeric identifier equal to theproject_idfield on thetender.completed/order.completedwebhooks (stringified). Round-trip the value verbatim toGET /projects/{id}.object_number— the ERP-facing grouping key. Not unique: Mercura groups multiple project rows that share the sameobject_numberinto one master project internally, so filtering byobject_numbermay return several rows.nameandstatus— display name and lifecycle state (ACTIVE,PROCESSED,BID_SUBMITTED,CUSTOMER_LOST,CUSTOMER_WON_PENDING,CUSTOMER_WON_AWARDED_ELSEWHERE,CUSTOMER_WON_AWARDED_TO_US).estimated_value/currency/submission_date— bid metadata as captured on the LV.responsible_user— the Mercura user (name + email) assigned to the project. Convenient for surfacing "assigned to Anna Schmidt" in a downstream inbox without a second lookup.construction_site_address/planner_address/developer_address— up to three postal addresses associated with the project. Fields not set on the internal row surface asnull.custom_fields— organisation-defined key-value pairs. Keys and value shapes are governed by your organisation's custom-column configuration (Settings → Organisation → Custom fields), not by this API. Multi-select fields return the option labels (e.g.["3030"]), not the internal option ids. Empty custom-field sets are omitted from the response entirely.request_ids— the Mercura request ids linked to the project via theRequest.project_idforeign key. Each id is request-keyed, so drill down to a tender withGET /tenders/{id}or to an order withGET /orders/{id}.acknowledgement— your side's last reported ERP/CRM import outcome for this project, when one was recorded viaPOST /projects/{project_id}/acknowledgements.nullwhen the project was never acknowledged. See Acknowledging a project below.
You can:
GET /projects— cursor-paginated list of projects for the authed organisation. Usemodified_sincefor delta-sync catch-up.GET /projects/{project_id}— fetch a single project. This is what thetender.completed/order.completedwebhooks (see the Webhooks chapter) point you at when partners need project-level context (site address, custom fields) beyond what the tender or order itself carries. The response carries a strongETagfor optimistic concurrency on PATCH.PATCH /projects/{project_id}— edit a project in place. The inbound counterpart of the otherwise Mercura-produced project: write your ERP's own commission / project number (and other metadata) back onto the Mercura object. See Updating a project below.POST /projects/{project_id}/acknowledgements— report whether your ERP/CRM managed to create/import the project (SUCCESSwith your own record id, orFAILEDwith a reason). ASUCCESSid is written onto the project'sobject_numbertoo, so this is also the method-minimal way to reconcile identifiers when your middleware cannot issue the PATCH verb. See Acknowledging a project below.
And Mercura can tell you when to act:
project.exported— the webhook a Mercura user triggers with the Send to ERP button on the project page. It is the signal to create the project as an object in your system; you then fetch it withGET /projects/{project_id}and report the result with an acknowledgement. See Creating a project in your ERP below.
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
nullclears that field.statusis the one exception — it cannot benull(a project always has a lifecycle state), and an unknown status value is a400. object_numberis 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_fieldsis label-keyed and shallow-merged. Use the same labels you receive onGET(and, for a multi-select, the same option labels). Keys you include are set; a key mapped tonullclears that one field; keys you omit are left as they are. An unknown label — or an unknown option label on a multi-select — returns400 VALIDATION_FAILEDnaming 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.
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" }
}