Mit den Artikel-Adressen der API hältst du deine Artikelstammdaten in X-Sitter aktuell, ohne sie von Hand zu pflegen: neue Artikel aus deinem Shop oder ERP anlegen, Namen, EANs oder Gewichte ändern, Artikel deaktivieren. Den Bestand liest du über eine eigene Adresse, siehe [Bestand abfragen](stock.md).

| Was | Methode und Adresse | Recht des Tokens |
|---|---|---|
| Artikel auflisten und suchen | `GET /api/v1/articles` | Artikel: lesen |
| Ein Artikel mit allen Feldern | `GET /api/v1/articles/{id}` | Artikel: lesen |
| Artikel anlegen | `POST /api/v1/articles` | Artikel: anlegen |
| Artikel ändern | `PATCH /api/v1/articles/{id}` | Artikel: ändern |
| Artikel löschen | `DELETE /api/v1/articles/{id}` | Artikel: löschen (nur Logistiker) |

## Schritt 1: Token mit den richtigen Rechten anlegen

Wie du einen Token anlegst, steht in der [Übersicht](overview.md#token-anlegen). Hake für Artikel unter **Artikel** nur an, was dein System wirklich tut:

- Nur lesen (z. B. die Artikelliste in dein ERP holen): **lesen**
- Artikel aus deinem Shop übertragen: **lesen**, **anlegen** und **ändern**

X-Sitter bietet diese Rechte nur an, wenn dein Benutzer die Artikelübersicht öffnen (für lesen) bzw. Artikel bearbeiten darf (für anlegen und ändern). **Löschen** steht Mandanten nicht zur Verfügung: Es bleibt beim Logistiker, der deine Artikel verwaltet.

## Schritt 2: Die Felder kennenlernen

Ein Artikel in X-Sitter besteht aus ein paar festen Angaben und beliebig vielen **Feldern** (Attributen) wie Name, EAN, Gewicht oder Zolltarifnummer. Welche Felder es gibt, legt dein Logistiker fest. Jedes Feld hat einen technischen Namen, den **Slug** (z. B. `name`, `ean`, `weight`). Mit diesem Slug liest und schreibst du den Wert.

Am einfachsten siehst du alle Felder, wenn du einen beliebigen einzelnen Artikel abfragst. Die ID bekommst du aus der Liste (Schritt 3):

```bash
curl "https://app.x-sitter.com/api/v1/articles/01999a3c-7e41-7b2a-9d0e-3f6c2a1b4e57" \
  -H "Authorization: Bearer xst_YOUR_TOKEN"
```

```json
{
  "data": {
    "id": "01999a3c-7e41-7b2a-9d0e-3f6c2a1b4e57",
    "sku": "SKU-100",
    "active": true,
    "company_id": 12,
    "managing_company_id": null,
    "owner_company_id": 27,
    "attributes": [
      { "slug": "name", "label": "Name", "type": "text", "value": "Orange juice 1 l" },
      { "slug": "ean", "label": "EAN", "type": "text", "value": "4260000000017" },
      { "slug": "weight", "label": "Gewicht", "type": "number", "value": "1.05" },
      { "slug": "color", "label": "Farbe", "type": "select", "value": null }
    ],
    "categories": [
      { "id": "01999a3c-2222-7b2a-9d0e-3f6c2a1b4e57", "name": "Juices", "path": "Beverages > Juices", "is_main": true }
    ],
    "groups": ["Beverages"],
    "added_at": "2026-09-30 10:12:44",
    "updated_at": "2026-10-06 08:01:13"
  }
}
```

| Feld | Bedeutung |
|---|---|
| `id` | Eindeutige ID des Artikels in X-Sitter. Speichere sie in deinem System, damit du den Artikel später direkt ändern kannst. |
| `sku` | Die Artikelnummer. |
| `active` | Ob der Artikel aktiv ist. |
| `company_id` | Firmennummer des Logistikers, der den Artikel verwaltet. |
| `owner_company_id` | Firmennummer des Mandanten, dem der Artikel gehört. `null` bei Artikeln des Logistikers selbst. |
| `managing_company_id` | Technische Angabe, für eine Anbindung nicht nötig. |
| `attributes` | Alle Felder des Logistikers mit Slug, Bezeichnung, Typ und dem Wert dieses Artikels (`null` = kein Wert). Die Bezeichnung ist die, die dein Logistiker dem Feld gegeben hat. |
| `categories` | Die Kategorien des Artikels mit ID und Pfad. `is_main` kennzeichnet die Hauptkategorie. |
| `groups` | Die Namen der Artikelgruppen, zu denen der Artikel gehört. |
| `added_at`, `updated_at` | Wann der Artikel angelegt und zuletzt geändert wurde. |

Notiere dir die Slugs der Felder, die dein System füllen soll. Fehlt dir ein Feld, bitte deinen Logistiker, es anzulegen. Felder lassen sich nicht über die API anlegen.

## Schritt 3: Artikel auflisten und suchen

```bash
curl "https://app.x-sitter.com/api/v1/articles?per_page=100&fields=name,ean" \
  -H "Authorization: Bearer xst_YOUR_TOKEN"
```

In der Liste kommen die Felder kompakt als `"slug": "value"`:

```json
{
  "data": [
    {
      "id": "01999a3c-7e41-7b2a-9d0e-3f6c2a1b4e57",
      "sku": "SKU-100",
      "active": true,
      "company_id": 12,
      "managing_company_id": null,
      "owner_company_id": 27,
      "attributes": { "name": "Orange juice 1 l", "ean": "4260000000017" },
      "added_at": "2026-09-30 10:12:44",
      "updated_at": "2026-10-06 08:01:13"
    }
  ],
  "meta": { "page": 1, "per_page": 100, "total": 312, "total_pages": 4 }
}
```

Als Mandant siehst du nur deine eigenen Artikel, als Logistiker alle Artikel, die du verwaltest. Die Liste kommt seitenweise; wie du alle Seiten abrufst, zeigt [Bestand abfragen, Schritt 5](stock.md#schritt-5-alle-seiten-abrufen). Tausch einfach die Adresse aus.

| Parameter | Beispiel | Wirkung |
|---|---|---|
| `fields` | `fields=name,ean,weight` | Welche Felder jeder Eintrag mitbringt (Slugs durch Kommas getrennt). Ohne den Parameter bekommst du die Standardfelder, die dein Logistiker eingestellt hat. |
| `q` | `q=Orange` | Sucht den Text in der Artikelnummer und in allen Feldwerten (Teiltreffer). |
| `attribute[slug]` | `attribute[ean]=4260000000017` | Nur Artikel, bei denen das Feld **genau** diesen Wert hat. Mehrere Filter werden kombiniert. |
| `active` | `active=true` | Nur aktive (`true`) oder nur inaktive (`false`) Artikel. |
| `category` | `category=01999a3c-2222-…` | Nur Artikel dieser Kategorie und ihrer Unterkategorien. |
| `sort` | `sort=added` | Sortierung: `sku` (Standard), `added` (Anlagedatum) oder der Slug eines Felds. |
| `direction` | `direction=desc` | `asc` (Standard) oder `desc`. |
| `page`, `per_page` | `per_page=500` | Seite und Einträge je Seite (Standard 25, höchstens 500). |

Einen Artikel über seine EAN finden:

```bash
curl -G "https://app.x-sitter.com/api/v1/articles" \
  -H "Authorization: Bearer xst_YOUR_TOKEN" \
  --data-urlencode "attribute[ean]=4260000000017"
```

!!!
Manche Werkzeuge verlangen, dass die eckigen Klammern in `attribute[ean]` URL-kodiert werden (`attribute%5Bean%5D=...`). Mit `curl -G` und `--data-urlencode` wie oben passiert das automatisch.
!!!

## Schritt 4: Artikel anlegen

Schick die Angaben als JSON. Pflicht ist nur die Artikelnummer `sku`, alles andere ist optional.

+++ cURL
```bash
curl -X POST "https://app.x-sitter.com/api/v1/articles" \
  -H "Authorization: Bearer xst_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "SKU-200",
    "active": true,
    "attributes": {
      "name": "Apple juice 1 l",
      "ean": "4260000000024",
      "weight": "1.05"
    },
    "categories": ["01999a3c-2222-7b2a-9d0e-3f6c2a1b4e57"]
  }'
```
+++ PHP
```php
<?php
$token = 'xst_YOUR_TOKEN';
$article = [
    'sku' => 'SKU-200',
    'active' => true,
    'attributes' => [
        'name' => 'Apple juice 1 l',
        'ean' => '4260000000024',
        'weight' => '1.05',
    ],
];

$ch = curl_init('https://app.x-sitter.com/api/v1/articles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($article),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status === 201 ? 'Created: ' . $result['data']['id'] : 'Error: ' . $result['error']['code'];
```
+++ Python
```python
import requests

token = "xst_YOUR_TOKEN"
article = {
    "sku": "SKU-200",
    "active": True,
    "attributes": {"name": "Apple juice 1 l", "ean": "4260000000024", "weight": "1.05"},
}

response = requests.post(
    "https://app.x-sitter.com/api/v1/articles",
    json=article,
    headers={"Authorization": f"Bearer {token}"},
    timeout=30,
)
result = response.json()
print(result["data"]["id"] if response.status_code == 201 else result["error"])
```
+++

Die Antwort hat den Status **201** und enthält den neuen Artikel in derselben Form wie in Schritt 2, einschließlich seiner `id`. Speichere die ID in deinem System.

| Angabe | Pflicht | Bedeutung |
|---|:-:|---|
| `sku` | ja | Artikelnummer, höchstens 255 Zeichen. |
| `active` | nein | `true` (Standard) oder `false`. |
| `attributes` | nein | Felder als `"slug": "value"`. Nur Slugs, die beim Logistiker existieren, werden angenommen. |
| `categories` | nein | Liste von Kategorie-IDs. Die erste ist die Hauptkategorie. Die IDs siehst du in Schritt 2 unter `categories[].id`. |
| `parent_id` | nein | Für Varianten: die Nummer des Elternartikels, so wie dein System sie nennt (höchstens 52 Zeichen). |
| `owner_company_id` | nein | Nur Logistiker: Firmennummer des Mandanten, dem der Artikel gehört. Leer oder `null` = Artikel des Logistikers. Als Mandant lässt du das Feld weg; deine Artikel gehören automatisch dir. |

So werden Feldwerte gespeichert:

- **Text:** wie geschickt, führende und nachfolgende Leerzeichen werden entfernt.
- **Zahl:** Komma und Punkt sind beide als Dezimaltrennzeichen erlaubt (`"1,05"` und `1.05`). Alles, was keine Zahl ist, wird mit `422 invalid_article_value` abgelehnt.
- **Ja/Nein:** `true` oder `false` werden zu `1` und `0`.
- **Mehrfachauswahl:** als Liste, z. B. `["red", "blue"]`; gespeichert als `red;blue`.
- **Auswahlfeld:** Schick den gespeicherten Wert der Option, nicht ihren Anzeigetext.

!!!warning Artikelnummer schon vergeben?
Eine Artikelnummer gibt es je Eigentümer nur einmal. Existiert sie schon, antwortet die API mit `409 article_number_taken` und nennt in `details.existing_id` die ID des vorhandenen Artikels. Ändere diesen Artikel (Schritt 5), statt einen zweiten anzulegen.
!!!

## Schritt 5: Artikel ändern

Mit `PATCH` änderst du **nur, was du schickst**. Alles andere bleibt, wie es ist.

```bash
curl -X PATCH "https://app.x-sitter.com/api/v1/articles/01999a3c-7e41-7b2a-9d0e-3f6c2a1b4e57" \
  -H "Authorization: Bearer xst_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": {
      "ean": "4260000000031",
      "color": null
    }
  }'
```

Das Beispiel setzt eine neue EAN und **entfernt** den Wert des Felds `color`. Die Antwort enthält den Artikel nach der Änderung.

- Ein Feld mit `null` (oder einem leeren Text) verliert seinen Wert.
- `categories` ersetzt die **komplette** Liste. Willst du eine Kategorie hinzufügen, schick die vorhandenen mit.
- Einen Artikel deaktivierst du mit `{"active": false}`. Er behält alle seine Daten und kann jederzeit wieder aktiviert werden.
- Auch die Artikelnummer lässt sich ändern (`{"sku": "SKU-200-NEW"}`), solange die neue noch nicht vergeben ist.

## Schritt 6: Dein System mit X-Sitter abgleichen

Für einen regelmäßigen Abgleich aus deinem Shop oder ERP hat sich dieses Vorgehen bewährt:

1. Für jeden Artikel aus deinem System: Kennst du schon seine X-Sitter-`id`? Dann schick ein `PATCH` mit den geänderten Feldern.
2. Kennst du sie nicht, schick ein `POST`.
3. Bekommst du `409 article_number_taken`, existiert der Artikel schon. Nimm die ID aus `details.existing_id`, speichere sie und schick ein `PATCH`.

```python
import requests

BASE = "https://app.x-sitter.com/api/v1/articles"
HEADERS = {"Authorization": "Bearer xst_YOUR_TOKEN"}


def upsert(article: dict, known_id: str | None) -> str:
    """Create or change one article, return its X-Sitter id."""
    if known_id is None:
        response = requests.post(BASE, json=article, headers=HEADERS, timeout=30)
        if response.status_code == 201:
            return response.json()["data"]["id"]
        error = response.json()["error"]
        if error["code"] != "article_number_taken":
            raise RuntimeError(f"{error['code']}: {error['message']}")
        known_id = error["details"]["existing_id"]

    changes = {key: value for key, value in article.items() if key != "sku"}
    response = requests.patch(f"{BASE}/{known_id}", json=changes, headers=HEADERS, timeout=30)
    if not response.ok:
        error = response.json()["error"]
        raise RuntimeError(f"{error['code']}: {error['message']}")
    return known_id
```

Schick nur Artikel, die sich seit dem letzten Abgleich geändert haben, nicht jedes Mal den kompletten Katalog.

## Schritt 7: Artikel löschen (nur Logistiker)

```bash
curl -X DELETE "https://app.x-sitter.com/api/v1/articles/01999a3c-7e41-7b2a-9d0e-3f6c2a1b4e57" \
  -H "Authorization: Bearer xst_YOUR_TOKEN"
```

Die Antwort hat den Status **204** ohne Inhalt.

!!!danger Löschen ist endgültig
Der Artikel wird sofort mit allen Feldwerten und Kategorien gelöscht und lässt sich nicht wiederherstellen. Liegt im Lager noch Bestand, wird nichts gelöscht: Die API antwortet mit `409 article_in_use` und nennt in `details.quantity` die Anzahl der Stück. Willst du einen Artikel nur nicht mehr verwenden, deaktiviere ihn stattdessen (`PATCH` mit `{"active": false}`).
!!!

## Für Logistiker: Artikel für einen Mandanten pflegen

Schick die Firmennummer des Mandanten im Header `X-Company`. Dann siehst du nur die Artikel dieses Mandanten, und neue Artikel gehören automatisch ihm:

```bash
curl -X POST "https://app.x-sitter.com/api/v1/articles" \
  -H "Authorization: Bearer xst_YOUR_TOKEN" \
  -H "X-Company: 27" \
  -H "Content-Type: application/json" \
  -d '{"sku": "SKU-300", "attributes": {"name": "Grape juice 1 l"}}'
```

Ohne `X-Company` kannst du den Eigentümer auch mit `owner_company_id` angeben. Dieselbe Artikelnummer darf es einmal beim Logistiker und einmal bei jedem Mandanten geben.

## Wenn etwas nicht funktioniert

| Antwort | Ursache | Lösung |
|---|---|---|
| `403 scope_missing` | Dem Token fehlt das Recht (lesen, anlegen, ändern oder löschen). | Lege einen neuen Token mit dem nötigen Recht an. |
| `400 invalid_json` | Der Body ist kein gültiges JSON-Objekt. | Prüfe das JSON (Anführungszeichen, Kommas) und schick `Content-Type: application/json`. |
| `400 invalid_parameter` | Ein Listen-Parameter hat das falsche Format, z. B. `active=maybe`. | Siehe die Tabelle in Schritt 3. |
| `404 article_not_found` | Die ID existiert nicht oder der Artikel gehört nicht zu deiner Firma. | Nimm die ID aus einer aktuellen Antwort. |
| `409 article_number_taken` | Die Artikelnummer existiert schon. | Ändere den vorhandenen Artikel aus `details.existing_id` (Schritt 6). |
| `409 article_in_use` | Der Artikel hat noch Bestand und kann nicht gelöscht werden. | Deaktiviere ihn, statt ihn zu löschen. |
| `422 invalid_article_value` | Ein Wert passt nicht, z. B. ein Feld, das es nicht gibt, keine Zahl in einem Zahlenfeld, eine unbekannte Kategorie oder eine fehlende `sku`. | Die `message` nennt das Feld. Vergleiche die Slugs mit Schritt 2. |

!!!
Angaben, die die API nicht kennt (z. B. ein Tippfehler wie `"attribute"` statt `"attributes"`), werden beim Anlegen und Ändern stillschweigend ignoriert. Prüfe deshalb nach deinem ersten Test in der Antwort, ob alle Werte angekommen sind.
!!!

Alle Fehler und Rechte auf einen Blick findest du in der [Übersicht](overview.md#fehler).
