# Mercura API

# Getting Started
## What this API is
**Mercura** is an AI-powered platform for processing inquiries — bills
of materials, tenders, RFQs — in wholesale distribution, manufacturing,
and technical sales. The **Mercura API** is the integration surface
your organisation uses to plug Mercura into the rest of your software
landscape.
The API is bidirectional by design:
- **Master data flows in** — your articles, customers, and suppliers
are pushed into Mercura so that incoming inquiries can be processed
against them.
- **Structured offers flow out** — each inquiry, once Mercura has read
the LV, captured the positions, and matched each one to an article
from your catalogue, is returned as a fully structured offer ready
for your downstream system to pick up.

The source system on your side does not have to be a specific ERP. Any
system that owns master data — an ERP, a PIM, a custom catalogue, a
spreadsheet export pipeline — can be the integration partner.
## Main use case
The end-to-end flow is four steps:
1. **Push master data.** `POST /articles`, `POST /customers`, and
`POST /suppliers` accept bulk payloads of up to 100,000 rows and
200 MiB each and return a `JobAck` immediately. A few large requests
are preferred over many small ones; for rich articles see
**Jobs → Request size**.
2. **Get notified when the jobs finish.** Each `POST` returns a
`job_id`. The recommended path is a **webhook**: register a
subscription once (admin UI, see **Webhooks** below) and Mercura
delivers a signed `job.finished` event to your endpoint as soon
as the job reaches a terminal state. If a webhook receiver is not
an option, fall back to polling `GET /jobs/{job_id}`.
3. **Read completed tenders and orders back.** This is the payoff. A
`tender.completed` (or `order.completed`) webhook fires the moment a
Mercura user releases a record to your system (the **Finalize** /
Export → API action) — your endpoint then calls `GET /tenders/{tender_id}`
(or `GET /orders/{order_id}`) to pull the full structured record
(positions, matched articles, quantities, prices, totals).
`GET /tenders` / `GET /orders` are the cursor-paginated list views
for catch-up scans; pass `?completed_since=…` for the completion feed.
4. **Acknowledge the import.** Once the record is committed to your
ERP/CRM, `POST /tenders/{tender_id}/acknowledgements` (or
`/orders/{order_id}/acknowledgements`) with `status: "SUCCESS"` and
your ERP document id in `external_id` — Mercura stores it on the
record's `erp_offer_id` (latest-wins) and surfaces the outcome in the
app. Report `status: "FAILED"` with a `message` when the import is
rejected.

A typical integration runs step 1 nightly (delta-sync new and changed
master-data rows) and reacts to `tender.completed` / `order.completed`
webhooks in step 3 to drive new offers into the downstream system in
real-time, acknowledging each import in step 4.
## Integration patterns: webhook vs polling
Steps 3–4 above can be driven two ways. **Webhook (push)** is the
recommended path — Mercura notifies your endpoint the instant a tender
or order completes, so you react in real time. **Polling (pull)** is the
fallback when your side cannot expose an inbound HTTPS endpoint — you
scan the completion feed on a schedule. Both end the same way: fetch the
full record, write it to your ERP/CRM, and acknowledge the import.
The sequences below show the **tender** flow. **Orders are identical** —
swap `tender.completed` → `order.completed`, `GET /tenders/{id}` →
`GET /orders/{id}`, and the acknowledgements path.
### Webhook (push)
![Webhook (push) sequence — Mercura user finalizes a record; Mercura POSTs an HMAC-signed tender.completed to your endpoint; you GET /tenders/{tender_id}, write the offer to your ERP, then POST the acknowledgement.](https://prod-euapi.mercura.ai/api/public/v1/docs-assets/webhook.svg)
The webhook payload is intentionally small — it identifies *which*
tender changed and *when*; you always fetch the current state with
`GET /tenders/{tender_id}`. See the **Webhooks** chapter for the wire
format, the HMAC-SHA256 signature recipe, and the retry schedule.
### Polling (pull)
![Polling (pull) sequence — your poller GETs /tenders?completed_since=… and pages via cursor; each item is a full tender; you write the offer to your ERP, then POST the acknowledgement.](https://prod-euapi.mercura.ai/api/public/v1/docs-assets/polling.svg)
Poll `?completed_since=…` on a schedule (every few minutes is typical).
`completed_at` refreshes on every completion, so a re-exported tender
reappears in the feed — key off `tender_id` and keep your ERP writes
idempotent. Keep `completed_since` on every request; once you hold a
`next_cursor`, send only the cursor.
## Webhooks
If you would rather **not poll**, Mercura can push events to an HTTPS
endpoint on your side as soon as something happens — a `job.finished`
event replaces the polling loop above, and a `tender.completed` /
`order.completed` event tells you the moment a record has been released
to your system (see **Integration patterns: webhook vs polling** above
for the full push-vs-pull sequence).
Subscriptions are set up by an org admin in the Mercura web app under
**Settings → Integrations → For developers → Webhooks** — there are no
`/webhook_subscriptions` endpoints in this API. Once a subscription
exists, the wire format, HMAC-SHA256 signature recipe, retry schedule,
and example payloads are documented in the **Webhooks** chapter
further down.
# Authentication
The API uses **bearer tokens** issued in the Mercura admin UI under
**Settings → Integrations → For developers → API**.

```
Authorization: Bearer mrc_live_<token>
```
Missing, malformed, expired, or revoked tokens return
`401 UNAUTHORIZED`.
# Quickstart
Six steps, one terminal, no SDK. By the end you will have pushed an
article into your catalogue, watched the ingestion job finish, received
a signed event the moment a tender was released, fetched the full
structured record, and acknowledged it — the exact loop a production
integration runs, at the smallest possible scale.
Steps 1–3 are plain `curl`. **Step 4 registers a webhook, and that is
the step that defines the shape of your integration** — Mercura pushes,
you react. Polling exists as a fallback and as a reconciliation sweep,
not as the main road.
Every step states the **checkpoint** that tells you it worked, so you
always know whether to continue or to stop and fix something.
## What you need
|  |  |
|  --- | --- |
| **An API key** | Mercura app → **Settings → Integrations → For developers → API**. It looks like `mrc_live_…` and is shown once — store it in your secret manager. |
| **The base URL** | `https://prod-euapi.mercura.ai/api/public/v1` |
| **An HTTPS endpoint** | For step 4 — reachable from the internet, able to answer `2xx` quickly. A tunnel to your laptop is fine while you are trying this out. No endpoint possible? Step 4 shows the polling fallback. |
| **One completed tender** | For steps 5–6. A Mercura user releases a tender with **Finalize** / *Export → API*; until that has happened at least once, there is nothing to fetch. |

Set the two values once, so every command below is copy-paste ready:

```bash
export MERCURA_API="https://prod-euapi.mercura.ai/api/public/v1"
export MERCURA_KEY="mrc_live_..."
```
## No key yet? Start against the mock server
This documentation site runs a **mock server** that answers the
operations in this reference with the example payloads shown on each
page. It accepts any API key and stores nothing, so you can build your
client before your organisation has issued a key, or point automated
tests at it that must never reach production. Swap the base URL and
every command in this Quickstart runs unchanged:

```bash
export MERCURA_API="https://docs.mercura.ai/_mock/openapi"
curl -sS "$MERCURA_API/articles"
```
The **Try it** console in this reference has the mock server
preselected. Know its limits before you rely on it:
- **Responses are the documented examples, not your data.**
`GET /articles/{article_number}` returns the same example article for
any number, and a `POST` returns a fixed `job_id` without storing
anything. The checkpoints below describe the real API, so they will
not match the mock.
- **Only path and query parameters are checked.** A missing required
parameter or a wrong type is rejected; a request body is not
validated, so a malformed body still gets the success example.
- **No webhooks.** The mock never calls your endpoint.

Switch `MERCURA_API` back to the production base URL when you have a key.
## Step 1 · Push one article
Master data goes in first — it is the catalogue that incoming inquiry
positions are matched against. The real sync sends thousands of rows per
request (see **Jobs → Request size**); here we send one.

```bash
curl -sS -X POST "$MERCURA_API/articles" \
  -H "Authorization: Bearer $MERCURA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-articles-1" \
  -d '{
        "articles": [
          {
            "article_number": "QS-1001",
            "name": "Ball valve DN25, brass, PN16",
            "manufacturer": "Acme",
            "manufacturer_article_number": "AC-25-BR",
            "ean_number": "4012345678901",
            "unit": "PCE",
            "list_price": "48.90",
            "custom_attributes": { "erp_material_group": "A150" }
          }
        ]
      }'
```

```json
{ "job_id": "4242", "status_url": "/api/public/v1/jobs/4242" }
```
**Checkpoint:** `202 Accepted` and a `job_id`. Nothing has been written
to the catalogue yet — every write in this API is asynchronous.
> Run the exact same command a second time: you get `200 OK` and the
**same** `job_id`, because the `Idempotency-Key` matched — no second
job runs. That is what makes retrying after a timeout or a dropped
connection safe. It also means the replay reports the original job's
outcome: if that job `FAILED`, so does the replay. Fix the cause, then
send the batch again under a **new** key.

## Step 2 · Wait for the job to finish

```bash
curl -sS "$MERCURA_API/jobs/4242" \
  -H "Authorization: Bearer $MERCURA_KEY"
```

```json
{
  "job_id": "4242",
  "entity": "ARTICLES",
  "status": "COMPLETED",
  "total_rows": 1,
  "created_count": 1,
  "updated_count": 0,
  "error_count": 0,
  "errors": []
}
```
**Checkpoint:** `status` is `COMPLETED` and `error_count` is `0`.
Two things to internalise here, because they shape your whole client:
- `COMPLETED` with `error_count > 0` is **partial success** — those rows
are listed in `errors[]` with their `row_number` and `identifier`, the
rest were applied. Only `FAILED` means nothing landed, and
`error_message` says why (for example a tag name that does not exist
in Mercura).
- Poll every second for the first 30 seconds, then back off to 5–10
seconds. Do not impose a wall-clock timeout — a 100k-row catalogue
sync legitimately takes minutes. See the **Jobs** chapter.

Once you have the webhook receiver from step 4 running, subscribe it to
**`job.finished`** as well and this polling loop disappears too — the
same push-instead-of-poll principle applies to both directions.
## Step 3 · Read the article back

```bash
curl -sS "$MERCURA_API/articles/QS-1001" \
  -H "Authorization: Bearer $MERCURA_KEY"
```
**Checkpoint:** `200 OK`, and the row carries what you sent — including
your `custom_attributes`. The article's public id **is** your
`article_number`: the identifier you assign stays the identifier you
address it by, in both directions, forever.
## Step 4 · Register a webhook — your trigger for everything below
Mercura tells you when a tender is ready; you do not go looking for it.
**This is the recommended integration pattern**, and it is worth wiring
up before you write any ERP code, because everything after this point is
a reaction to it.
Register a subscription in the Mercura app under **Settings →
Integrations → For developers → Webhooks**, subscribe to `tender.completed`, and store the
signing secret it gives you. Then fire the **`webhook.test`** event from
the same screen to prove your receiver is reachable before real events
depend on it.
Your receiver has three jobs:
1. **Verify `Mercura-Signature-256`** against the raw request bytes —
before trusting anything in the payload. The HMAC-SHA256 recipe and a
ready-made verifier are in the **Webhooks** chapter.
2. **Deduplicate on `Mercura-Event-Id`.** It is stable across retries;
the same event may legitimately arrive twice.
3. **Answer `2xx` fast**, then do the ERP work asynchronously. A `5xx`
or a timeout puts the delivery on the retry curve.

**Checkpoint:** `webhook.test` arrives at your endpoint and its
signature verifies. From here on, a Mercura user hitting **Finalize** /
*Export → API* delivers you a `tender.completed` event:

```json
{
  "event_id": "7e8f4a3b-9c1d-4a2b-9d1c-2e3f4a5b6c7d",
  "event_type": "tender.completed",
  "delivered_at": "2026-07-09T12:00:00Z",
  "data": {
    "tender_id": 12345,
    "name": "RFQ-2026-0042 — Office Tower B",
    "position_count": 47,
    "completed_at": "2026-07-09T11:59:58Z"
  }
}
```
The payload is deliberately small — it tells you *which* tender changed,
not what is in it. Take `data.tender_id` into step 5.
> **If you cannot expose an inbound HTTPS endpoint**, poll the completion
feed instead — same trigger, minutes later instead of seconds:

```bash
curl -sS "$MERCURA_API/tenders?completed_since=2026-01-01T00:00:00Z" \
  -H "Authorization: Bearer $MERCURA_KEY"
```
Each item in `data[]` is already a full tender, so you can skip step 5;
`next_cursor` pages through the rest.

**Run that same feed as a nightly reconciliation sweep even once
webhooks are live.** Push plus reconcile is what survives an outage on
either side — it is not redundancy you can skip.
## Step 5 · Fetch the tender

```bash
curl -sS "$MERCURA_API/tenders/12345" \
  -H "Authorization: Bearer $MERCURA_KEY"
```
**Checkpoint:** you get `chapters[]` → `positions[]` with
`article_number`, `quantity`, `unit`, `net_price`, `line_net`, plus
`totals`. That structure is what you map into an ERP offer.
Always fetch the current state rather than trusting a cached copy — the
webhook is a notification, not a snapshot. Three things that save you a
bug later:
- **`net_total` is the authoritative amount**, not `positions_subtotal`
— the subtotal is a derived helper that excludes alternatives and
Bedarfspositionen, while `net_total` matches the PDF the customer sees.
- **`positions[].id` is opaque.** Use it as the handle when you edit a
line; never derive it from the position number.
- **`customer_id` and `project_id` are references only.** Resolve them
via `GET /customers/{customer_id}` and `GET /projects/{project_id}`.

## Step 6 · Acknowledge the import
Close the loop: tell Mercura what your ERP did with the offer, so the
operator sees the outcome in the app instead of asking you.

```bash
curl -sS -X POST "$MERCURA_API/tenders/12345/acknowledgements" \
  -H "Authorization: Bearer $MERCURA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "SUCCESS", "external_id": "SO-2026-5567" }'
```
**Checkpoint:** `200 OK` with the recorded acknowledgement. Your
`external_id` is written to the tender's `erp_offer_id` and shows up
everywhere Mercura reads that field.
When the import is rejected, report it — `message` is required in that
case and is what the operator reads:

```bash
curl -sS -X POST "$MERCURA_API/tenders/12345/acknowledgements" \
  -H "Authorization: Bearer $MERCURA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "FAILED", "message": "ERP rejected customer KFR003 (credit hold)" }'
```
You now have the complete loop a production integration runs: master
data in, a signed event out, the full record fetched, the outcome
reported back.
## Where to go next
| If you want to… | Go to |
|  --- | --- |
| Sync a whole catalogue nightly | **Jobs** — batching, idempotency, per-row errors |
| Pull only what changed | `modified_since` in **Read-endpoint pagination** |
| Improve match quality | **Articles** — the fields that drive matching |
| Correct prices or quantities on a tender | `PATCH /tenders/{tender_id}` in **Tenders** |
| Handle failures properly | **Errors**, **Rate limits**, and the retry policy in **Webhooks** |
| Exchange files instead of calling the API | **SFTP-based integration** at the end of this page |

# Operations
## Data conventions
A few conventions hold across every resource:
- **Embedded addresses, standalone contacts.** Customer and
supplier payloads carry their `addresses[]` inline — there is no
separate `/addresses` endpoint, and the wire format hides the
internal address table so each parent push is a single snapshot.
**Contacts**, by contrast, are managed via the dedicated `/contacts`
resource and carry their own `parent_type` (`customer` or `supplier`)
and `parent_id`, so adding or updating a contact does not require
re-sending the entire parent — see the **Contacts** chapter for the
full model and upsert rules.
- **Stable partner-supplied identifiers.** `article_number`,
`customer_id`, and `supplier_id` are the keys you assign in your
source system. They must remain stable across sync cycles — Mercura
uses them to upsert: same id → existing row updated; new id → new
row created.
- **Timestamps in ISO-8601 UTC.** All timestamps on the wire are
ISO-8601 with an explicit `Z` (or `+00:00`) suffix.
- **`custom_fields` on every master-data resource.** Use it for any
source-system field that doesn't fit the standard schema. Mercura
preserves the value alongside the row; matching uses it where
appropriate.

## Errors
Every non-2xx response uses one envelope:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request validation failed",
    "details": [ { "loc": ["query", "cursor"], "msg": "Invalid cursor", "type": "value_error" } ],
    "request_id": "9a3b...e2"
  }
}
```
Branch on `code` — it is stable. `message` is human-readable and may
change between versions. The full list of codes (`VALIDATION_FAILED`,
`UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `IDEMPOTENCY_KEY_MISMATCH`,
`RATE_LIMITED`, `INTERNAL_ERROR`, …) is documented per endpoint.
## Request IDs
Every response carries an `X-Request-Id` header (echoed if you supply
one, generated otherwise) and the same id is embedded in every error
envelope. Quote it in support tickets — it is the fastest way for
Mercura's team to look up the exact request in our logs.
## Versioning
The API follows **Semantic Versioning** (`MAJOR.MINOR.PATCH`). Only the
`MAJOR` number lives in the URL path (`/v1`, future `/v2`, …). The full
SemVer string is published in the OpenAPI `info.version` field and on
every response as the `X-API-Version` header.
| Bump | What it means |
|  --- | --- |
| **PATCH** | Bugfix / doc fix. OpenAPI shape unchanged. |
| **MINOR** | Additive only: new optional field, new endpoint, relaxed constraint. Existing clients keep working unchanged. |
| **MAJOR** | Breaking: removed field, renamed field, narrower constraint. A new URL prefix is published; the previous `MAJOR` remains available during a migration period. |

## Rate limits
Each API key has its own budget. **Every request counts as one** against
the matching limiter — reads draw from the read bucket, writes from the
write bucket, and each in-flight bulk-write job holds one concurrency
slot. Defaults today:
| Class | Limit |
|  --- | --- |
| Writes (POST) | 60 / minute, burst 10 |
| Reads (GET) | 600 / minute, burst 60 |
| Concurrent in-flight bulk-write jobs | 5 per key |

Every response carries `RateLimit-Policy` / `RateLimit` headers (and
legacy `X-RateLimit-*` headers) describing your current budget. On a
`429 RATE_LIMITED` response, `Retry-After` tells you how long to wait,
and `X-RateLimit-Scope` tells you whether you hit the per-minute bucket
(`rate`) or the concurrent-jobs cap (`concurrency`).
## Read-endpoint pagination
Every `GET` list endpoint — `/tenders`, `/orders`, `/supplier-requests`,
`/articles`, `/customers`, `/suppliers`, `/projects`, and `/contacts` — is cursor
paginated. Pass `?modified_since=<ISO-8601>` on the
first page to bound the lower edge of the scan by `updated_at` (useful for
delta-sync). Mercura returns a page plus a `next_cursor` — pass it back as
`?cursor=…` to get the next page. Once you have a cursor, `modified_since`
is ignored.
On `/tenders` and `/orders` you can instead pass
`?completed_since=<ISO-8601>` to pull the **completion feed** — only records
completed at/after that instant, ordered by `completed_at` — the
reconciliation twin of the `tender.completed` / `order.completed` webhooks.
Both accept a `status` filter; `/contacts` additionally filters by
`parent_type` / `parent_id` / `is_active`.
The cursor is keyed on `(updated_at, id)`, so it is stable under
concurrent writes: a row updated mid-scan may resurface on a later
page, but you will never silently skip or duplicate rows.
# Alternative integration
## SFTP-based integration
The Mercura API documented here is the preferred channel because it is
synchronous, low-latency, and operationally light — status comes back
immediately, no file-watching is required on either side, and
master-data updates can flow event-driven. Where a direct API
integration is not feasible (network constraints, security policy,
ERP capability), Mercura also supports SFTP-based exchange in both
directions for the same payloads. Talk to your Mercura contact if you
need that path; the data shapes are identical to the ones documented
here.

Version: 1.79.2

## Servers

Production
```
https://prod-euapi.mercura.ai/api/public/v1
```

## Download OpenAPI description

 - [Mercura API](https://docs.mercura.ai/_bundle/openapi.yaml)

## Articles

 - [Data depth and match quality](https://docs.mercura.ai/openapi/articles/data-depth-and-match-quality.md): ### Data depth and match quality The precision of Mercura's article matching depends directly on the **depth and structure** of the article data you push. The more you send, the more accurately Mercur
 - [About ](https://docs.mercura.ai/openapi/articles/about.md): ### About `article_number` Article numbers are partner-supplied and must match the character class `[A-Za-z0-9._\-]{1,255}` so they round-trip safely through URL path segments. `/`, `?`, `#`, and whit
 - [Tags](https://docs.mercura.ai/openapi/articles/tags.md): ### Tags Articles carry **tags** — short labels (e.g. `"LED"`, `"Eigenmarke"`) your team manages in the Mercura admin UI under **Settings → Labels & text templates** (*Article labels*). Send them by *
 - [ETIM features](https://docs.mercura.ai/openapi/articles/etim-features.md): ### ETIM features Push ETIM classification features to sharpen how Mercura matches incoming LV positions against your catalogue. Each feature is one object: ```json { "article_number": "LEU-1500-50
 - [Translations (multilingual catalogues)](https://docs.mercura.ai/openapi/articles/translations-(multilingual-catalogues).md): ### Translations (multilingual catalogues) If your catalogue is maintained in several languages, send the translated display texts alongside each article in a `translations` map, keyed by **lowercase
 - [Soft-deleting articles](https://docs.mercura.ai/openapi/articles/soft-deleting-articles.md): ### Soft-deleting articles Set `is_deleted: true` on an article in a `POST /articles` payload to **hide it everywhere in Mercura** — it stops appearing in catalogue search, AI matching suggestions, an
 - [Lifecycle flags](https://docs.mercura.ai/openapi/articles/lifecycle-flags.md): ### Lifecycle flags Two more boolean flags are accepted on `POST /articles` and returned on the read endpoints (both default `false`): - **`is_diverse`** — marks a catch-all / placeholder ("diverse")
 - [POST /articles](https://docs.mercura.ai/openapi/articles/public_articles_bulk_upsert.md): Bulk-upsert articles (async). Accepts up to 100,000 articles per call. The body is validated synchronously; the actual upsert runs out-of-band on a worker. **Response.** - `202 Accepted` with `JobAck
 - [GET /articles](https://docs.mercura.ai/openapi/articles/public_articles_list.md): Cursor-paginated list of articles.
 - [GET /articles/{article_number}](https://docs.mercura.ai/openapi/articles/public_articles_get.md): Fetch one article by its partner-supplied `article_number`.
## Accessories

 - [Fields](https://docs.mercura.ai/openapi/accessories/fields.md): ### Fields - **`multiplier`** — how many of the accessory are needed per unit of the source article (optional). - **`order`** — display order among a source article's accessories. - **`is_mandatory`**
 - [Upsert semantics (v1)](https://docs.mercura.ai/openapi/accessories/upsert-semantics-(v1).md): ### Upsert semantics (v1) `POST` **inserts or updates** each `(source, accessory)` pair; it never removes links. Re-sending the same pair is idempotent. There is **no delete endpoint yet** — omitting
 - [POST /articles/accessories](https://docs.mercura.ai/openapi/accessories/public_accessories_bulk_upsert.md): Bulk-upsert accessory relationships (async). Accepts up to 100,000 rows per call. Each row upserts the `(source_article_number, accessory_article_number)` pair; rows whose articles don't exist are ski
 - [GET /articles/accessories](https://docs.mercura.ai/openapi/accessories/public_accessories_list.md): Cursor-paginated list of accessory relationships.
## Alternatives

 - [Fields](https://docs.mercura.ai/openapi/alternatives/fields.md): ### Fields - **`order`** — display order among a source article's alternatives (optional).
 - [Upsert semantics (v1)](https://docs.mercura.ai/openapi/alternatives/upsert-semantics-(v1).md): ### Upsert semantics (v1) `POST` **inserts or updates** each `(source, alternative)` pair; it never removes links. Re-sending the same pair is idempotent. There is **no delete endpoint yet** — omittin
 - [POST /articles/alternatives](https://docs.mercura.ai/openapi/alternatives/public_alternatives_bulk_upsert.md): Bulk-upsert alternative relationships (async). Accepts up to 100,000 rows per call. Each row upserts the `(source_article_number, alternative_article_number)` pair; rows whose articles don't exist are
 - [GET /articles/alternatives](https://docs.mercura.ai/openapi/alternatives/public_alternatives_list.md): Cursor-paginated list of alternative relationships.
## Successors

 - [One successor per article](https://docs.mercura.ai/openapi/successors/one-successor-per-article.md): ### One successor per article A source article has **exactly one** active successor. Re-sending a source article with a different `successor_article_number` **replaces** the previous successor — this
 - [Upsert semantics (v1)](https://docs.mercura.ai/openapi/successors/upsert-semantics-(v1).md): ### Upsert semantics (v1) There is **no delete endpoint yet**. A row whose source or successor `article_number` does not exist in your catalogue is **skipped** (counted in the job's `skipped_count`),
 - [POST /articles/successors](https://docs.mercura.ai/openapi/successors/public_successors_bulk_upsert.md): Bulk-upsert successor relationships (async). Accepts up to 100,000 rows per call. Successors are 1:1 per source article: re-sending a source with a new successor replaces the previous one. Rows whose
 - [GET /articles/successors](https://docs.mercura.ai/openapi/successors/public_successors_list.md): Cursor-paginated list of successor relationships.
## Unit Conversions

 - [The ratio](https://docs.mercura.ai/openapi/unit-conversions/the-ratio.md): ### The ratio The conversion is `1 alternative_unit = (numerator / denominator) base units`. Both must be positive. The ratio is stored and returned exactly as sent — it is not collapsed to a single f
 - [Upsert semantics (v1)](https://docs.mercura.ai/openapi/unit-conversions/upsert-semantics-(v1).md): ### Upsert semantics (v1) `POST` **inserts or updates** each `(article, alternative_unit)` conversion; it never removes conversions you stop sending, and there is **no delete endpoint yet**. A row who
 - [POST /articles/unit-conversions](https://docs.mercura.ai/openapi/unit-conversions/public_unit_conversions_bulk_upsert.md): Bulk-upsert unit conversions (async). Accepts up to 100,000 rows per call. Each row upserts the `(article_number, alternative_unit)` conversion; rows whose article doesn't exist are skipped (see the j
 - [GET /articles/unit-conversions](https://docs.mercura.ai/openapi/unit-conversions/public_unit_conversions_list.md): Cursor-paginated list of unit conversions.
## Customer Article Numbers

 - [Keys](https://docs.mercura.ai/openapi/customer-article-numbers/keys.md): ### Keys A mapping is identified by **`customer_id` + `customer_article_number`**. Re-sending a pair with a different `article_number` re-points it to that article. `customer_id` is the same id you se
 - [Replacing a customer's list](https://docs.mercura.ai/openapi/customer-article-numbers/replacing-a-customer's-list.md): ### Replacing a customer's list By default the call only adds and updates. To remove mappings a customer no longer uses, send that customer's **complete** list with `"replace_customers": true`: for ev
 - [POST /articles/customer-article-numbers](https://docs.mercura.ai/openapi/customer-article-numbers/public_customer_article_numbers_bulk_upsert.md): Bulk-upsert customer article numbers (async). Accepts up to 100,000 rows per call. A mapping is keyed by `(customer_id, customer_article_number)`; re-sending a key with a different `article_number` re
 - [GET /articles/customer-article-numbers](https://docs.mercura.ai/openapi/customer-article-numbers/public_customer_article_numbers_list.md): Cursor-paginated list of customer-article-number mappings.
## Customers

 - [About ](https://docs.mercura.ai/openapi/customers/about.md): ### About `customer_id` Customer ids are partner-supplied and must match `[A-Za-z0-9._\-]{1,255}` so they round-trip safely through URL path segments.
 - [About ](https://docs.mercura.ai/openapi/customers/about.md): ### About `custom_fields` Read responses (`GET /customers`, `GET /customers/{customer_id}`, and the `customer` block of every offer) return `custom_fields` keyed by the field's display **label** — the
 - [POST /customers](https://docs.mercura.ai/openapi/customers/public_customers_bulk_upsert.md): Bulk-upsert customers (async). Accepts up to 100,000 customers per call. The body is validated synchronously; the actual upsert runs out-of-band on a worker. **Response.** - `202 Accepted` with `JobAc
 - [GET /customers](https://docs.mercura.ai/openapi/customers/public_customers_list.md): Cursor-paginated list of customers.
 - [GET /customers/{customer_id}](https://docs.mercura.ai/openapi/customers/public_customers_get.md): Fetch one customer by its partner-supplied `customer_id`.
## Suppliers

 - [About ](https://docs.mercura.ai/openapi/suppliers/about.md): ### About `supplier_id` Supplier ids are partner-supplied and must match `[A-Za-z0-9._\-]{1,255}` so they round-trip safely through URL path segments. Up to v1.6.x this field was called `external_id`
 - [About ](https://docs.mercura.ai/openapi/suppliers/about.md): ### About `custom_fields` Read responses (`GET /suppliers`, `GET /suppliers/{supplier_id}`) return `custom_fields` keyed by the field's display **label** — the same label you see in the Mercura admin
 - [POST /suppliers](https://docs.mercura.ai/openapi/suppliers/public_suppliers_bulk_upsert.md): Bulk-upsert suppliers (async). Accepts up to 100,000 suppliers per call. The body is validated synchronously; the actual upsert runs out-of-band on a worker. **Response.** - `202 Accepted` with `JobAc
 - [GET /suppliers](https://docs.mercura.ai/openapi/suppliers/public_suppliers_list.md): Cursor-paginated list of suppliers.
 - [GET /suppliers/{supplier_id}](https://docs.mercura.ai/openapi/suppliers/public_suppliers_get.md): Fetch one supplier by its partner-supplied `supplier_id`.
## Contacts

 - [Upsert key](https://docs.mercura.ai/openapi/contacts/upsert-key.md): ### Upsert key Each contact may carry its own optional `external_id` — the stable identifier from your source ERP (for example, Clage's `ASP_ID`). The upsert priority within a parent is: 1. `external_
 - [Custom fields](https://docs.mercura.ai/openapi/contacts/custom-fields.md): ### Custom fields A contact carries your organisation's contact custom columns in `custom_fields`, keyed by the column **label** — the same keys in both directions, so a value you read on `GET` can be
 - [POST /contacts](https://docs.mercura.ai/openapi/contacts/public_contacts_bulk_upsert.md): Bulk-upsert contacts (async). Each row carries its own `parent_type` (`customer` or `supplier`) and `parent_id` (the parent's partner-supplied identifier — `customer_id` for customers, `supplier_id` f
 - [GET /contacts](https://docs.mercura.ai/openapi/contacts/public_contacts_list.md): Cursor-paginated list of contacts.
 - [GET /contacts/{contact_id}](https://docs.mercura.ai/openapi/contacts/public_contacts_get.md): Fetch one contact by its Mercura id.
 - [PATCH /contacts/{contact_id}](https://docs.mercura.ai/openapi/contacts/public_contacts_patch.md): Partial update of a contact. Only fields present in the payload are applied. Reassigning the parent is not supported — delete the contact and POST a new one.
 - [DELETE /contacts/{contact_id}](https://docs.mercura.ai/openapi/contacts/public_contacts_delete.md): Soft-delete a contact (`is_active=false`).
## Orders

 - [Creating an order](https://docs.mercura.ai/openapi/orders/creating-an-order.md): ## Creating an order `POST /orders` is the programmatic twin of forwarding a purchase-order email: send the order document(s) and Mercura runs the same intake pipeline. Accepted files, size caps, prim
 - [Reading orders](https://docs.mercura.ai/openapi/orders/reading-orders.md): ## Reading orders `GET /orders` and `GET /orders/{order_id}` return an order's matched line items plus order-specific metadata. Keyed by the **request id** — an order has a single result, so `order_id
 - [Completion feed](https://docs.mercura.ai/openapi/orders/completion-feed.md): ### Completion feed Pass **`completed_since`** (an ISO-8601 instant) to pull every order completed recently — the typical ERP write-back trigger: ``` GET /orders?completed_since=2026-07-09T11:55:00Z
 - [Export file](https://docs.mercura.ai/openapi/orders/export-file.md): ### Export file To collect the order as a file — the same file the Export menu in Mercura, a `FILE` webhook and the SFTP outbox produce — name the format: ``` GET /orders/{order_id}/file?format=order
 - [Following an order before it is finished](https://docs.mercura.ai/openapi/orders/following-an-order-before-it-is-finished.md): ### Following an order before it is finished The completion feed and the `order.completed` webhook both report the *end* of an order's life. To follow it on the way there, subscribe to **`order.status
 - [Acknowledging an order](https://docs.mercura.ai/openapi/orders/acknowledging-an-order.md): ## Acknowledging an order - **`POST /orders/{order_id}/acknowledgements`** — after you import an order into your ERP, report the result back so the Mercura operator can see it. Send `status: "SUCCESS"
 - [Reporting lifecycle events](https://docs.mercura.ai/openapi/orders/reporting-lifecycle-events.md): ## Reporting lifecycle events - **`POST /orders/{order_id}/events`** — report a lifecycle event your own system recorded for an order *after* the initial acknowledgement: the order was **Cancelled**,
 - [POST /orders](https://docs.mercura.ai/openapi/orders/public_orders_create.md): Create an order from uploaded files (async processing). The programmatic twin of forwarding a purchase-order email: Mercura stores the files, extracts the order metadata (customer, references, deliver
 - [GET /orders](https://docs.mercura.ai/openapi/orders/public_orders_list.md): Cursor-paginated list of orders for the authed organisation.
 - [GET /orders/{order_id}](https://docs.mercura.ai/openapi/orders/public_orders_get.md): Fetch one order by its public id.
 - [GET /orders/{order_id}/file](https://docs.mercura.ai/openapi/orders/public_orders_get_file.md): Download the order's export file in the given format. The same file the app's Export menu, a `FILE` webhook and the SFTP outbox produce for that format, rendered on request. Poll the completion feed (
 - [POST /orders/{order_id}/acknowledgements](https://docs.mercura.ai/openapi/orders/public_orders_acknowledge.md): Report the ERP import outcome for one order. The partner POSTs `status` (`SUCCESS` / `FAILED`), optionally an `external_id` (its own order/record id) and `metadata`, and a `message` (required on `FAIL
 - [POST /orders/{order_id}/events](https://docs.mercura.ai/openapi/orders/public_orders_record_event.md): Report a lifecycle event your system recorded for one order. Append-only: each POST records one event (e.g. the order was *Cancelled* in your ERP), so the full partner-side timeline is preserved. Supp
## Tenders

 - [Creating a tender](https://docs.mercura.ai/openapi/tenders/creating-a-tender.md): ## Creating a tender `POST /tenders` is the programmatic twin of forwarding an LV / RFQ email to `anfragen@lv.mercura.ai`: send the same files you would attach to the mail and Mercura runs the identic
 - [Carrying your own reference and routing](https://docs.mercura.ai/openapi/tenders/carrying-your-own-reference-and-routing.md): ### Carrying your own reference and routing Two optional form fields let a CRM / ERP hand over the context Mercura cannot infer from the files: | Field | What it does | | --- | --- | | `custom_fields
 - [Reading tenders](https://docs.mercura.ai/openapi/tenders/reading-tenders.md): ## Reading tenders A tender is **keyed by the request id** (`tender_id == request_id`): reading one returns the tender's *current* projected state — the structured, priced result Mercura produces — ca
 - [Line order](https://docs.mercura.ai/openapi/tenders/line-order.md): ### Line order Number your lines by **`sequence`**, not by the array order and not by `position_number`. A chapter's own lines can stand *between* its sub-chapters — a few lines, then a sub-chapter, t
 - [Removed lines and positions](https://docs.mercura.ai/openapi/tenders/removed-lines-and-positions.md): ### Removed lines and positions By default a tender contains only what is currently on it: a line removed after you imported the tender simply stops appearing, which leaves you unable to tell it apart
 - [The complete Leistungsverzeichnis](https://docs.mercura.ai/openapi/tenders/the-complete-leistungsverzeichnis.md): ### The complete Leistungsverzeichnis By default a tender contains only its **priced** content: an ordinal number Mercura matched no article to, one whose articles were all taken off again, and one it
 - [Review notes (](https://docs.mercura.ai/openapi/tenders/review-notes-(.md): ### Review notes (`annotations`) While it works through a tender, Mercura's matching agent — or a reviewer — can attach **review notes** to an ordinal number: a special condition found in the LV ("all
 - [Tender documents](https://docs.mercura.ai/openapi/tenders/tender-documents.md): ## Tender documents `GET /tenders/{tender_id}/documents` lists the **source documents** the customer uploaded into the tender — the same attachments a forwarded LV / RFQ email carried (GAEB, PDF, Exce
 - [Document feed](https://docs.mercura.ai/openapi/tenders/document-feed.md): ### Document feed `GET /tenders/documents` returns the same documents for **all** tenders of your organisation as a cursor-paginated feed ordered by `(updated_at, id)` — the same `modified_since` / `c
 - [Editing a tender](https://docs.mercura.ai/openapi/tenders/editing-a-tender.md): ## 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
 - [Closing a tender](https://docs.mercura.ai/openapi/tenders/closing-a-tender.md): ### Closing a tender Not every tender that reaches Mercura is worked in Mercura. A small one may be quoted by hand, or it may belong to a product line that is not onboarded — and its case is then clos
 - [Acknowledging a tender](https://docs.mercura.ai/openapi/tenders/acknowledging-a-tender.md): ## Acknowledging a tender - **`POST /tenders/{tender_id}/acknowledgements`** — after you import a tender's offer into your ERP/CRM, report the result back so the Mercura operator can see it. Send `sta
 - [Reporting lifecycle events](https://docs.mercura.ai/openapi/tenders/reporting-lifecycle-events.md): ## Reporting lifecycle events - **`POST /tenders/{tender_id}/events`** — report a lifecycle event your own system recorded for a tender *after* the initial acknowledgement: the tender was marked **Won
 - [Completion feed](https://docs.mercura.ai/openapi/tenders/completion-feed.md): ### Completion feed To pull every tender that was completed recently — the typical ERP write-back trigger — pass **`completed_since`** (an ISO-8601 instant): ``` GET /tenders?completed_since=2026-07-
 - [Export file](https://docs.mercura.ai/openapi/tenders/export-file.md): ### Export file To collect the tender as a file — the same file the Export menu in Mercura, a `FILE` webhook and the SFTP outbox produce — name the format: ``` GET /tenders/{tender_id}/file?format=x8
 - [POST /tenders](https://docs.mercura.ai/openapi/tenders/public_tenders_create.md): Create a tender from uploaded files (async processing). The programmatic twin of forwarding an LV / RFQ email to `anfragen@lv.mercura.ai`: Mercura stores the files, picks the file that drives parsing
 - [GET /tenders](https://docs.mercura.ai/openapi/tenders/public_tenders_list.md): Cursor-paginated list of tenders for the authed organisation.
 - [GET /tenders/documents](https://docs.mercura.ai/openapi/tenders/public_tenders_document_feed.md): Feed of every tender's source documents, ordered by when they last changed. Poll it with `modified_since` to archive documents without walking each tender. A document comes back whenever its record ch
 - [GET /tenders/{tender_id}](https://docs.mercura.ai/openapi/tenders/public_tenders_get.md): Fetch one tender by its public id. The response carries a strong `ETag` over the tender content; pass it back as `If-Match` on `PATCH /tenders/{tender_id}` for optimistic concurrency. The ETag covers
 - [PATCH /tenders/{tender_id}](https://docs.mercura.ai/openapi/tenders/public_tenders_update.md): 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` t
 - [GET /tenders/{tender_id}/documents](https://docs.mercura.ai/openapi/tenders/public_tenders_list_documents.md): List the customer's source documents attached to a tender. Returns metadata for each **original uploaded** file (GAEB / PDF / Excel / image / e-mail) together with a short-lived, pre-signed `download_
 - [GET /tenders/{tender_id}/file](https://docs.mercura.ai/openapi/tenders/public_tenders_get_file.md): Download the tender's export file in the given format. The same file the app's Export menu, a `FILE` webhook and the SFTP outbox produce for that format, rendered on request. Poll the completion feed
 - [POST /tenders/{tender_id}/acknowledgements](https://docs.mercura.ai/openapi/tenders/public_tenders_acknowledge.md): Report the ERP/CRM import outcome for one tender. The partner POSTs `status` (`SUCCESS` / `FAILED`), optionally an `external_id` (its own offer/record id) and `metadata`, and a `message` (required on
 - [POST /tenders/{tender_id}/events](https://docs.mercura.ai/openapi/tenders/public_tenders_record_event.md): Report a lifecycle event your system recorded for one tender. Append-only: each POST records one event (e.g. the tender was marked *Won* or *Lost* in your CRM), so the full partner-side timeline is pr
## Supplier Requests

 - [Reading supplier requests](https://docs.mercura.ai/openapi/supplier-requests/reading-supplier-requests.md): ## Reading supplier requests A supplier request is keyed by its own **UUID** (`id`) and carries: - **`status`** — lifecycle state: `REQUEST_SENT` (sent, awaiting an offer), `OFFER_RECEIVED` (the suppl
 - [Filtering](https://docs.mercura.ai/openapi/supplier-requests/filtering.md): ## Filtering All filters are optional and composable: - **`tender_id`** — only supplier requests raised for one tender. This is the authoritative "all supplier requests for this tender" query: ``` GE
 - [Joining to a tender](https://docs.mercura.ai/openapi/supplier-requests/joining-to-a-tender.md): ## Joining to a tender Given a tender, `GET /supplier-requests?tender_id={id}` lists every Werksanfrage raised for it. Within each, every `offered_lines[].position_id` points at the position that line
 - [GET /supplier-requests](https://docs.mercura.ai/openapi/supplier-requests/public_supplier_requests_list.md): Cursor-paginated list of supplier requests for the authed organisation.
 - [GET /supplier-requests/{supplier_request_id}](https://docs.mercura.ai/openapi/supplier-requests/public_supplier_requests_get.md): Fetch one supplier request by its public id (its UUID).
## Projects

 - [Updating a project](https://docs.mercura.ai/openapi/projects/updating-a-project.md): ## 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 i
 - [Creating a project in your ERP](https://docs.mercura.ai/openapi/projects/creating-a-project-in-your-erp.md): ## Creating a project in your ERP Projects are created inside Mercura (from a GAEB file, from an email intake, or by a user), so — unlike tenders — there is no partner path that creates one. What you
 - [Acknowledging a project](https://docs.mercura.ai/openapi/projects/acknowledging-a-project.md): ## Acknowledging a project `POST /projects/{project_id}/acknowledgements` records your ERP/CRM import outcome on the project — the project-keyed sibling of the tender/order acknowledgements. Use it wh
 - [Filtering the list](https://docs.mercura.ai/openapi/projects/filtering-the-list.md): ## Filtering the list Three filters are AND-combined with `modified_since` on the list endpoint: | Query parameter | Behaviour | | --- | --- | | `object_number=<string>` | Exact match on the ERP grou
 - [Custom fields](https://docs.mercura.ai/openapi/projects/custom-fields.md): ## Custom fields `custom_fields` is a free-form map shaped by your organisation's own custom-column configuration — Mercura does not enforce a JSON schema on the contents. Keys and value formats can e
 - [Relationship to tenders and orders](https://docs.mercura.ai/openapi/projects/relationship-to-tenders-and-orders.md): ## Relationship to tenders and orders - One project → many requests (`Request.project_id`). - Each request is reachable by its id as a tender (`GET /tenders/{id}`) or an order (`GET /orders/{id}`); th
 - [GET /projects](https://docs.mercura.ai/openapi/projects/public_projects_list.md): Cursor-paginated list of projects for the authed organisation.
 - [GET /projects/{project_id}](https://docs.mercura.ai/openapi/projects/public_projects_get.md): Fetch one project by its internal numeric id. The id matches a request's `project_id` — pass the value verbatim (stringified) to round-trip. The response carries a strong `ETag` over the editable cont
 - [PATCH /projects/{project_id}](https://docs.mercura.ai/openapi/projects/public_projects_update.md): 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`, whi
 - [POST /projects/{project_id}/acknowledgements](https://docs.mercura.ai/openapi/projects/public_projects_acknowledge.md): Report the ERP/CRM import outcome for one project. The partner POSTs `status` (`SUCCESS` / `FAILED`), optionally an `external_id` (its own project/record id, e.g. its ERP document number) and `metadat
## Jobs

 - [The async write loop](https://docs.mercura.ai/openapi/jobs/the-async-write-loop.md): ## The async write loop ``` POST /articles Authorization: Bearer mrc_live_... Idempotency-Key: nightly-sync-2026-05-19 Content-Type: application/json { "articles": [ ... up to 100,000 rows ... ] } →
 - [Request size](https://docs.mercura.ai/openapi/jobs/request-size.md): ## Request size Two limits apply to every bulk write; whichever you reach first counts: - **Rows:** at most 100,000 per request. - **Bytes:** the request body may be at most **200 MiB** (209,715,200 b
 - [Status enum](https://docs.mercura.ai/openapi/jobs/status-enum.md): ## Status enum | `status` | Meaning | | --- | --- | | `PENDING` | Queued, not yet picked up by a worker. | | `RUNNING` | A worker is processing rows. | | `COMPLETED` | Finished. If `error_count > 0`,
 - [Rejected rows](https://docs.mercura.ai/openapi/jobs/rejected-rows.md): ## Rejected rows Mercura checks every row again while it applies the batch, after the request validation that answers `400`. A row it rejects is never counted as applied: - **Customers:** a rejected r
 - [Polling cadence](https://docs.mercura.ai/openapi/jobs/polling-cadence.md): ## Polling cadence Most jobs finish in seconds; a 100,000-row catalogue sync may take a few minutes. A reasonable client polls every **1 second for the first 30 seconds**, then backs off to **5–10 sec
 - [Idempotency](https://docs.mercura.ai/openapi/jobs/idempotency.md): ## Idempotency Pass an `Idempotency-Key` header (any string up to 255 characters, e.g. `erp-2026-05-18-batch-1`) on bulk-write requests. Mercura keys jobs on `(organisation, Idempotency-Key)`: - **Sam
 - [Finding out what went wrong](https://docs.mercura.ai/openapi/jobs/finding-out-what-went-wrong.md): ## Finding out what went wrong | What you see | Where the reason is | | --- | --- | | `413 PAYLOAD_TOO_LARGE` on the `POST` itself | The body is over 200 MiB (see **Request size**). Nothing was read
 - [Errors](https://docs.mercura.ai/openapi/jobs/errors.md): ## Errors - `404 NOT_FOUND` — the `job_id` is unknown, malformed, or belongs to another organisation. We deliberately do not distinguish these cases: the existence of another tenant's jobs is never le
 - [GET /jobs/{job_id}](https://docs.mercura.ai/openapi/jobs/public_jobs_get.md): Fetch the status of an async ingestion job.
## Webhooks

 - [Setup](https://docs.mercura.ai/openapi/webhooks/setup.md): # Setup
 - [Delivery](https://docs.mercura.ai/openapi/webhooks/delivery.md): # Delivery
 - [Receiver implementation](https://docs.mercura.ai/openapi/webhooks/receiver-implementation.md): # Receiver implementation
 - [GET /document-links](https://docs.mercura.ai/openapi/webhooks/public_document_links_download.md): Download one document of a `tender.completed` / `order.completed` delivery. Use the `download_url` exactly as the delivery carries it. A link works until its `download_url_expires_at`; every delivery
## Salesforce

 - [How the connection works](https://docs.mercura.ai/openapi/salesforce/how-the-connection-works.md): ## How the connection works - **Authentication.** Mercura signs in to your External Client App with OAuth 2.0 (client credentials flow). Salesforce runs every call as the app's **Run-As user**, so Mer
 - [Setup in Salesforce](https://docs.mercura.ai/openapi/salesforce/setup-in-salesforce.md): ## Setup in Salesforce The setup takes a Salesforce administrator about half an hour. The steps are also listed on the Salesforce page in Mercura. 1. **Create an integration user.** Under *Setup → Use
 - [Permissions of the integration user](https://docs.mercura.ai/openapi/salesforce/permissions-of-the-integration-user.md): ### Permissions of the integration user | Data flow | Permissions | | --- | --- | | Customers and Contacts | Read on Account and Contact and on the fields Mercura should take over | | Requests | Read
 - [Connecting in Mercura](https://docs.mercura.ai/openapi/salesforce/connecting-in-mercura.md): ## Connecting in Mercura In Mercura, open **Settings → Integrations → Salesforce**. Under **Access**, enter your org's My Domain URL, the consumer key and the consumer secret, then click **Save and te
 - [Configuring the data flows](https://docs.mercura.ai/openapi/salesforce/configuring-the-data-flows.md): ## Configuring the data flows Under **Which data is exchanged** you switch each data flow on separately. Fields and values are picked from lists that Mercura reads from your org. Changes apply after *
 - [Customers and Contacts](https://docs.mercura.ai/openapi/salesforce/customers-and-contacts.md): ### Customers and Contacts Accounts become Mercura customers, keyed by the Account ID, with phone and billing address. Contacts come with name, e-mail and phone. With **Extra fields** you fill further
 - [Requests from Salesforce](https://docs.mercura.ai/openapi/salesforce/requests-from-salesforce.md): ### Requests from Salesforce Records of one object become Mercura requests together with their files. Usually this is the Opportunity; Case, Lead, Contract, Work Order or a custom object work as well.
 - [Status to Salesforce and cancel from Salesforce](https://docs.mercura.ai/openapi/salesforce/status-to-salesforce-and-cancel-from-salesforce.md): ### Status to Salesforce and cancel from Salesforce **Status to Salesforce** writes the request's Mercura status into a field of its record whenever it changes, for example the Opportunity stage, the
 - [Quote to Salesforce](https://docs.mercura.ai/openapi/salesforce/quote-to-salesforce.md): ### Quote to Salesforce When a user exports the request in Mercura, Mercura writes the result under the request's Opportunity, with one line per position. As the **target** you choose: - **Quote:** Me
 - [Opportunities for other requests](https://docs.mercura.ai/openapi/salesforce/opportunities-for-other-requests.md): ### Opportunities for other requests Requests that reach Mercura by e-mail or another channel can also get their Quote in Salesforce, as long as their customer comes from Salesforce. On export, the us
 - [Orders from Salesforce](https://docs.mercura.ai/openapi/salesforce/orders-from-salesforce.md): ### Orders from Salesforce Draft Orders with a file become Mercura orders for the customer of their Account. By default every Order in the Draft category is taken in; a rule and a send flag narrow thi
 - [Activity and errors](https://docs.mercura.ai/openapi/salesforce/activity-and-errors.md): ## Activity and errors At the bottom of the page, **Salesforce activity** lists every record taken in: when it was checked, the outcome (for example "Request created", "Waiting for customer", "No docu
 - [Notes and limits](https://docs.mercura.ai/openapi/salesforce/notes-and-limits.md): ## Notes and limits - **One channel per source.** Records that Mercura takes from Salesforce should not also reach Mercura by e-mail forwarding or through your own API client; otherwise requests are c
 - [Getting started](https://docs.mercura.ai/openapi/salesforce/getting-started.md): ## Getting started 1. Create the integration user and the External Client App in a sandbox. 2. Enter the credentials in your Mercura test organisation and test the connection. 3. Switch on the data fl
## SAP Business One

 - [How the connection works](https://docs.mercura.ai/openapi/sap-business-one/how-the-connection-works.md): ## How the connection works - **Service Layer.** Mercura uses SAP Business One's standard REST interface, the Service Layer (OData v3 at `/b1s/v1` or OData v4 at `/b1s/v2`). - **Authentication.** Merc
 - [Setup in SAP Business One](https://docs.mercura.ai/openapi/sap-business-one/setup-in-sap-business-one.md): ## Setup in SAP Business One 1. **Create a technical user** for Mercura, e.g. "MERCURA", in the company database Mercura should work with. A dedicated user keeps Mercura's changes apart in the change
 - [Authorisations of the technical user](https://docs.mercura.ai/openapi/sap-business-one/authorisations-of-the-technical-user.md): ### Authorisations of the technical user | Data flow | Authorisations | | --- | --- | | Articles and prices | Read on Item Master Data, Item Groups and Price Lists | | Customers and contact persons |
 - [Connecting in Mercura](https://docs.mercura.ai/openapi/sap-business-one/connecting-in-mercura.md): ## Connecting in Mercura In Mercura, open **Settings → Integrations → SAP Business One**. Under **Access**, enter the **Service Layer URL** (ending in `/b1s/v1` or `/b1s/v2`), the **company database**
 - [Configuring the data flows](https://docs.mercura.ai/openapi/sap-business-one/configuring-the-data-flows.md): ## Configuring the data flows Under **Which data is exchanged** you switch each data flow on separately. Changes apply after **Save**.
 - [Articles and prices](https://docs.mercura.ai/openapi/sap-business-one/articles-and-prices.md): ### Articles and prices Items become Mercura articles, keyed by the item code (`ItemCode`): | Mercura | SAP Business One | | --- | --- | | Article number | `ItemCode` | | Name | `ItemName` | | Unit |
 - [Customers and contact persons](https://docs.mercura.ai/openapi/sap-business-one/customers-and-contact-persons.md): ### Customers and contact persons Business partners of type customer become Mercura customers, keyed by the customer code (`CardCode`), with name, e-mail, VAT ID, phone, postal code, city and country.
 - [Documents](https://docs.mercura.ai/openapi/sap-business-one/documents.md): ### Documents **Quotations:** when a tender is completed in Mercura, Mercura creates a quotation in SAP Business One. **Orders:** when an order is completed in Mercura, Mercura creates a sales order.
 - [Activity and errors](https://docs.mercura.ai/openapi/sap-business-one/activity-and-errors.md): ## Activity and errors **Activity** at the top of the page opens the connection with every pull and every document sent: when it ran, how many records were read and which were rejected, and the answer
 - [Notes and limits](https://docs.mercura.ai/openapi/sap-business-one/notes-and-limits.md): ## Notes and limits - **One source per data.** Articles and customers that Mercura takes from SAP Business One should not also reach Mercura by another import; otherwise they overwrite each other. - *
 - [Getting started](https://docs.mercura.ai/openapi/sap-business-one/getting-started.md): ## Getting started 1. Create the technical user in a test company database. 2. Enter the access in your Mercura test organisation and save to test the connection. 3. Switch on the pulls you need and c
## SAP S/4HANA

 - [How the connection works](https://docs.mercura.ai/openapi/sap-s4hana/how-the-connection-works.md): ## How the connection works - **Standard APIs.** Mercura uses SAP's released OData v2 APIs from the SAP Business Accelerator Hub, the same in every S/4HANA system. Your own fields (`YY1_…`, `ZZ1_…`) a
 - [Setup in SAP](https://docs.mercura.ai/openapi/sap-s4hana/setup-in-sap.md): ## Setup in SAP The setup is done by your SAP Basis team. 1. **Activate the OData services** in transaction `/IWFND/MAINT_SERVICE`, only those of the data flows you use: `API_SALES_ORDER_SRV`, `API_SA
 - [Authorisations of the technical user](https://docs.mercura.ai/openapi/sap-s4hana/authorisations-of-the-technical-user.md): ### Authorisations of the technical user | Data flow | Authorisations | | --- | --- | | All | Start authorisation for each service used (`S_SERVICE`) | | Customers and contact persons | Display busin
 - [Connecting in Mercura](https://docs.mercura.ai/openapi/sap-s4hana/connecting-in-mercura.md): ## Connecting in Mercura In Mercura, open **Settings → Integrations → SAP S/4HANA**. Under **Access**, enter: - **Host:** scheme, host and port of SAP Gateway, without a path, e.g. `https://s4.example
 - [Configuring the data flows](https://docs.mercura.ai/openapi/sap-s4hana/configuring-the-data-flows.md): ## Configuring the data flows Changes apply after **Save**.
 - [Sales area](https://docs.mercura.ai/openapi/sap-s4hana/sales-area.md): ### Sales area Every document is booked in one **sales organisation**, **distribution channel** and **division**. The distribution channel can follow the sold-to's country: under **Distribution channe
 - [Customers and contact persons](https://docs.mercura.ai/openapi/sap-s4hana/customers-and-contact-persons.md): ### Customers and contact persons Business partners with a customer record become Mercura customers, keyed by the SAP customer number, the same number sales orders name as sold-to party. Mercura takes
 - [Articles](https://docs.mercura.ai/openapi/sap-s4hana/articles.md): ### Articles Products become Mercura articles, keyed by the product number: | Mercura | SAP S/4HANA | | --- | --- | | Article number | `Product` | | Name | Product description in the chosen **languag
 - [Sales orders](https://docs.mercura.ai/openapi/sap-s4hana/sales-orders.md): ### Sales orders When an order is completed in Mercura, Mercura creates a sales order in SAP with the lines an ERP export of the order has: - **Header:** order type, sales area, the customer as sold-t
 - [Sales quotations](https://docs.mercura.ai/openapi/sap-s4hana/sales-quotations.md): ### Sales quotations When a tender is completed in Mercura, Mercura creates a sales quotation in SAP with the **quotation type** (`AG` by default), the sales area, the customer as sold-to party and on
 - [For both documents](https://docs.mercura.ai/openapi/sap-s4hana/for-both-documents.md): ### For both documents - **Reference field and prefix.** Each document carries the Mercura reference: the prefix (`MERCURA-REQ-` by default) and the request number. The reference goes into `Assignment
 - [Activity and errors](https://docs.mercura.ai/openapi/sap-s4hana/activity-and-errors.md): ## Activity and errors **Activity** at the top of the page opens the connection with every pull and every document sent: when it ran, how many records were read and which were rejected, and the answer
 - [Notes and limits](https://docs.mercura.ai/openapi/sap-s4hana/notes-and-limits.md): ## Notes and limits - **One source per data.** Customers and articles that Mercura takes from SAP should not also reach Mercura by another import; otherwise they overwrite each other. - **No customer
 - [Getting started](https://docs.mercura.ai/openapi/sap-s4hana/getting-started.md): ## Getting started 1. Activate the services and create the technical user in your test system (e.g. the quality system). 2. Enter the access in your Mercura test organisation and save to test the conn
## Open Masterdata

 - [What you need from your wholesaler](https://docs.mercura.ai/openapi/open-masterdata/what-you-need-from-your-wholesaler.md): ## What you need from your wholesaler | What | Example | Notes | | --- | --- | --- | | Service address | `https://omd.wholesaler.example/openmasterdata/v1.1.0` | Up to and including the version segme
 - [Settings](https://docs.mercura.ai/openapi/open-masterdata/settings.md): ## Settings - **Package format.** OMD 1.1.0 and older expect the requested data packages joined (`datapackage=basic|prices`), 1.2.0 and newer one parameter per package. A wrong format is silent: the s
 - [The delta file](https://docs.mercura.ai/openapi/open-masterdata/the-delta-file.md): ## The delta file Place one JSON file per batch of changes in `omd_delta/`. An empty list is valid. ```json { "products": [ { "change": "new", "manufacturerID": "40526", "manufacturerIDType"
 - [Runs and troubleshooting](https://docs.mercura.ai/openapi/open-masterdata/runs-and-troubleshooting.md): ## Runs and troubleshooting The **Runs** tab of the Open Masterdata resource lists the last delta files with their counts — changes, fetched, not found, collective articles skipped, failed — and **Set
## Users

 - [Typical use: resolve a tender's responsible user](https://docs.mercura.ai/openapi/users/typical-use:-resolve-a-tender's-responsible-user.md): ### Typical use: resolve a tender's responsible user `GET /tenders` returns `user_email` — the email of the Mercura user who released the tender to the API. Feed that value into `GET /users?email=<use
 - [custom_fields](https://docs.mercura.ai/openapi/users/custom_fields.md): ### `custom_fields` Organisation-defined custom fields are returned keyed by their display **label** (for example `"SAP ID"`), not by the internal column id. Fields with no active column definition ar
 - [Service accounts](https://docs.mercura.ai/openapi/users/service-accounts.md): ### Service accounts The API service account backing your integration is never returned by either endpoint.
 - [GET /users](https://docs.mercura.ai/openapi/users/public_users_list_by_email.md): Look a user up by email. Returns a `CursorPage` with the matching user(s), or an empty `data` array when the email is unknown in your organisation (never a 404 — the status code doesn't reveal which e
 - [GET /users/{user_id}](https://docs.mercura.ai/openapi/users/public_users_get.md): Fetch one user by their Mercura user id.
## Statistics

 - [Selection accuracy](https://docs.mercura.ai/openapi/statistics/selection-accuracy.md): ## Selection accuracy `GET /statistics/selections` returns, for every request in a date range, how that request's positions were resolved into selections. Rows cover both request types shown on the da
 - [Status & handling time](https://docs.mercura.ai/openapi/statistics/status-and-handling-time.md): ## Status & handling time Each row also reports where the request sits in Mercura's processing lifecycle, the true handling time spent on it, and the timestamps around it: - `status` — the lifecycle s
 - [Exported vs. in-progress requests](https://docs.mercura.ai/openapi/statistics/exported-vs.-in-progress-requests.md): ## Exported vs. in-progress requests By default only **exported**, non-deleted, non-evaluation requests are included — the same universe as the dashboard. Pass **`include_unexported=true`** to also re
 - [Date range](https://docs.mercura.ai/openapi/statistics/date-range.md): ## Date range `start_date` and `end_date` (both `YYYY-MM-DD`, required) bound the range on the request's **creation date**, inclusive on both ends. The window is capped at **400 days**; an inverted ra
 - [Pagination](https://docs.mercura.ai/openapi/statistics/pagination.md): ## Pagination Standard cursor pagination (see *Getting Started → Pagination*). Rows are ordered newest-first by request creation time. Pass `limit` (1–500, default 100); when a further page exists the
 - [GET /statistics/selections](https://docs.mercura.ai/openapi/statistics/public_statistics_selections.md): Per-request selection-accuracy and processing-time statistics. Returns the same raw data as the in-app "Exported requests overview" dashboard: for each request in the range, how its positions' selecti
## SCIM Provisioning

 - [Endpoint](https://docs.mercura.ai/openapi/scim-provisioning/endpoint.md): ## Endpoint | Setting | Value | | --- | --- | | Base URL (EU) | `https://scim.mercura.ai/scim/v2` — in Entra ID this is the *Tenant URL*; also shown on the settings page | | Resources | `/Users`, `/G
 - [Authentication](https://docs.mercura.ai/openapi/scim-provisioning/authentication.md): ## Authentication Requests carry a bearer token that an admin of your organisation creates under **Settings → Integrations → For developers → SCIM provisioning → Provisioning tokens**: ``` Authorizat
 - [Users](https://docs.mercura.ai/openapi/scim-provisioning/users.md): ## Users | SCIM attribute | Use in Mercura | | --- | --- | | `userName` | The user's e-mail address and Mercura identity (required); see below for directories where it is not an e-mail address | | `e
 - [Groups and roles](https://docs.mercura.ai/openapi/scim-provisioning/groups-and-roles.md): ## Groups and roles Provisioned users get one of the roles **Admin**, **Member** or **Contributor**; anyone without a matching group is a **Member**. Mercura assigns roles from groups, using the mappi
 - [Filtering](https://docs.mercura.ai/openapi/scim-provisioning/filtering.md): ## Filtering | Resource | Attributes | Operators | | --- | --- | --- | | `/Users` | `userName`, `externalId`, `displayName` | `eq`, `ne`, `co`, `sw`, `ew`, `pr` | | `/Users` | `active` | `eq`, `ne`,
 - [Setting up Microsoft Entra ID](https://docs.mercura.ai/openapi/scim-provisioning/setting-up-microsoft-entra-id.md): ## Setting up Microsoft Entra ID 1. In Mercura, open **Settings → Integrations → For developers → SCIM provisioning**, copy the base URL and create a token. 2. In the Entra admin center, open **Enterp
