# Articles, variants and units

Articles are the owner's catalogue. Stock remains a separate quantity view. External references belong to one API connection; neither a matching name nor a matching SKU authorizes a different article binding. Article responses contain no purchasing costs or margins.

| Method and path | Scope | Result |
|---|---|---|
| GET /v1/articles | products:read | Paginated own catalogue |
| GET /v1/articles/{id} | products:read | Article, revision, variants and connection references |
| POST /v1/articles/upsert | products:write | Durable article/reference receipt |
| POST /v1/articles/variants/commands | products:write | Durable native variant receipt |
| GET /v1/units | products:read | Supported unit definitions |

## Read and synchronize

The list accepts `after` (unchanged `next_cursor`), `limit` from 1 to 100 (default 50), `search` up to 160 characters and `include_archived=true|false`. Continue while `has_more` is true. Pages are current catalogue reads, not a frozen catalogue snapshot. Archive status is explicit; a missing page is never a deletion signal. Test keys return an empty test catalogue and cannot mutate real articles.

The detail's `revision` is required for updating or explicitly binding an existing article. `external_references` belongs only to the calling connection. Variant `stock_article_id` identifies the actual physical article; several choices can intentionally share it. Existing orders preserve their frozen stock binding after later catalogue changes.

## Import an article

```json
{
  "idempotency_key": "11111111-1111-4111-8111-111111111111",
  "external_article_id": "ERP-CUP-42",
  "external_variant_id": "",
  "article": { "name": "Cup", "sku": "CUP-42", "is_active": true }
}
```

Use `article_id` to deliberately bind an existing own article and include its current `expected_revision`. An existing external reference is never silently reassigned. Creation requires a name. Unspecified fields remain unchanged; explicit null clears only fields that accept null. Existing SKU/EAN/manufacturer values are not silently replaced when they conflict. See OpenAPI for the exact nullable fields, sizes and supported catalogue attributes.

The response contains `command_id`, `idempotency_key`, the saved `article`, `created`, `reference_created` and `created_at`. A lost response is recovered by repeating the **exact same input and key**, including the original revision. The saved receipt is checked before a new write. Reusing that key with other input returns `idempotency_conflict`.

## Variants

The variant command contains `idempotency_key`, `article_id` and `operation`. `operation.kind=create` accepts `name`, `options`, `mappedProductId`, `sku` and `ean`. `kind=update` additionally requires `variant_id` and `expected_revision` and can change `isActive`. Only native catalogue variants are mutable here; imported provider variants retain their own authority. Options have explicit names and values. `mappedProductId=null` uses the parent article's physical stock, not a new stock pool.

## Units and quantities

Article `base_unit=piece` points to the unit catalogue: name `Stück`, six decimal places, factor `1`, revision `1`. Stock quantities are exact decimal strings. No implicit pack, length, area or weight conversion is performed. The published original inventory contract retains its existing unit label; do not reinterpret old quantities because the article catalogue now exposes a unit code.

## Recovery

A revision conflict requires rereading and reviewing the change. An uncertain write (`command_outcome_unknown`) requires an exact retry, not a new identity. A catalogue failure is never an empty or zero-stock response. Writes use live keys; end-to-end write testing belongs in the isolated simulator.
