With the article addresses of the API you keep your article master data in X-Sitter up to date without maintaining it by hand: create new articles from your shop or ERP, change names, EANs or weights, deactivate articles. You read the stock through a separate address, see [Query stock](stock.md).

| What | Method and address | Token permission |
|---|---|---|
| List and search articles | `GET /api/v1/articles` | Articles: read |
| One article with all fields | `GET /api/v1/articles/{id}` | Articles: read |
| Create an article | `POST /api/v1/articles` | Articles: create |
| Change an article | `PATCH /api/v1/articles/{id}` | Articles: change |
| Delete an article | `DELETE /api/v1/articles/{id}` | Articles: delete (logisticians only) |

## Step 1: Create a token with the right permissions

How to create a token is described in the [overview](overview.md#create-a-token). For articles, tick under **Articles** (German: *Artikel*) only what your system really does:

- Read only (e.g. fetch the article list into your ERP): **read** (*lesen*)
- Transfer articles from your shop: **read**, **create** (*anlegen*) and **change** (*ändern*)

X-Sitter only offers these permissions if your user may open the product overview (for read) or edit products (for create and change). **Delete** is not available to clients: it stays with the logistician who manages your articles.

## Step 2: Get to know the fields

An article in X-Sitter consists of a few fixed details and any number of **fields** (attributes) such as name, EAN, weight or customs tariff number. Your logistician decides which fields exist. Each field has a technical name, the **slug** (e.g. `name`, `ean`, `weight`). You read and write the value with this slug.

The easiest way to see all fields is to query any single article. You get the ID from the list (step 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"
  }
}
```

| Field | Meaning |
|---|---|
| `id` | Unique ID of the article in X-Sitter. Store it in your system so you can change the article directly later. |
| `sku` | The article number. |
| `active` | Whether the article is active. |
| `company_id` | Company number of the logistician who manages the article. |
| `owner_company_id` | Company number of the client that owns the article. `null` for articles of the logistician itself. |
| `managing_company_id` | Technical detail, not needed for an integration. |
| `attributes` | All fields of the logistician with slug, label, type and the value of this article (`null` = no value). The label is the one your logistician gave the field. |
| `categories` | The categories of the article with ID and path. `is_main` marks the main category. |
| `groups` | The names of the article groups the article belongs to. |
| `added_at`, `updated_at` | When the article was created and last changed. |

Note the slugs of the fields your system should fill. If you are missing a field, ask your logistician to create it. Fields cannot be created through the API.

## Step 3: List and search articles

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

In the list, the fields come in compact form as `"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 }
}
```

As a client you only see your own articles, as a logistician all articles you manage. The list comes page by page; how to fetch all pages is shown in [Query stock, step 5](stock.md#step-5-fetch-all-pages). Just swap the address.

| Parameter | Example | Effect |
|---|---|---|
| `fields` | `fields=name,ean,weight` | Which fields each entry carries (slugs separated by commas). Without the parameter you get the default fields your logistician has set. |
| `q` | `q=Orange` | Searches the text in the article number and in all field values (partial match). |
| `attribute[slug]` | `attribute[ean]=4260000000017` | Only articles where the field has **exactly** this value. Several filters are combined. |
| `active` | `active=true` | Only active (`true`) or only inactive (`false`) articles. |
| `category` | `category=01999a3c-2222-…` | Only articles of this category and its subcategories. |
| `sort` | `sort=added` | Sort order: `sku` (default), `added` (creation date) or the slug of a field. |
| `direction` | `direction=desc` | `asc` (default) or `desc`. |
| `page`, `per_page` | `per_page=500` | Page and entries per page (default 25, at most 500). |

Find an article by its EAN:

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

!!!
Some tools require the square brackets in `attribute[ean]` to be URL-encoded (`attribute%5Bean%5D=...`). With `curl -G` and `--data-urlencode` as above this happens automatically.
!!!

## Step 4: Create an article

Send the details as JSON. Only the article number `sku` is required, everything else is 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"])
```
+++

The response has the status **201** and contains the new article in the same form as in step 2, including its `id`. Store the ID in your system.

| Detail | Required | Meaning |
|---|:-:|---|
| `sku` | yes | Article number, at most 255 characters. |
| `active` | no | `true` (default) or `false`. |
| `attributes` | no | Fields as `"slug": "value"`. Only slugs that exist at the logistician are accepted. |
| `categories` | no | List of category IDs. The first one is the main category. You can see the IDs in step 2 under `categories[].id`. |
| `parent_id` | no | For variants: the number of the parent article as your system names it (at most 52 characters). |
| `owner_company_id` | no | Logisticians only: company number of the client that owns the article. Empty or `null` = article of the logistician. As a client, leave the field out; your articles automatically belong to you. |

This is how field values are stored:

- **Text:** as sent, leading and trailing spaces are removed.
- **Number:** comma and point are both allowed as decimal separator (`"1,05"` and `1.05`). Anything that is not a number is rejected with `422 invalid_article_value`.
- **Yes/no:** `true` or `false` become `1` and `0`.
- **Multiple choice:** as a list, e.g. `["red", "blue"]`; stored as `red;blue`.
- **Selection field:** send the stored value of the option, not its display text.

!!!warning Article number already taken?
An article number exists only once per owner. If it already exists, the API answers with `409 article_number_taken` and names the ID of the existing article in `details.existing_id`. Change that article (step 5) instead of creating a second one.
!!!

## Step 5: Change an article

With `PATCH` you change **only what you send**. Everything else stays as it is.

```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
    }
  }'
```

The example sets a new EAN and **removes** the value of the field `color`. The response contains the article after the change.

- A field with `null` (or an empty text) loses its value.
- `categories` replaces the **complete** list. If you want to add a category, send the existing ones as well.
- You deactivate an article with `{"active": false}`. It keeps all its data and can be activated again at any time.
- The article number can be changed too (`{"sku": "SKU-200-NEW"}`), as long as the new one is not taken yet.

## Step 6: Sync your system with X-Sitter

For a regular sync from your shop or ERP, this procedure has proven itself:

1. For every article from your system: do you already know its X-Sitter `id`? Then send a `PATCH` with the changed fields.
2. If you do not know it, send a `POST`.
3. If you get `409 article_number_taken`, the article already exists. Take the ID from `details.existing_id`, store it and send a `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
```

Only send articles that have changed since the last sync, not the complete catalogue every time.

## Step 7: Delete an article (logisticians only)

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

The response has the status **204** without content.

!!!danger Deleting is permanent
The article is deleted immediately together with all field values and categories and cannot be restored. If there is still stock in the warehouse, nothing is deleted: the API answers with `409 article_in_use` and names the number of pieces in `details.quantity`. If you just no longer want to use an article, deactivate it instead (`PATCH` with `{"active": false}`).
!!!

## For logisticians: maintain articles for a client

Send the client's company number in the header `X-Company`. Then you only see that client's articles, and new articles automatically belong to it:

```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"}}'
```

Without `X-Company` you can also name the owner with `owner_company_id`. The same article number may exist once at the logistician and once at each client.

## If something does not work

| Response | Cause | Solution |
|---|---|---|
| `403 scope_missing` | The token lacks the permission (read, create, change or delete). | Create a new token with the required permission. |
| `400 invalid_json` | The body is not a valid JSON object. | Check the JSON (quotes, commas) and send `Content-Type: application/json`. |
| `400 invalid_parameter` | A list parameter has the wrong format, e.g. `active=maybe`. | See the table in step 3. |
| `404 article_not_found` | The ID does not exist or the article does not belong to your company. | Take the ID from a current response. |
| `409 article_number_taken` | The article number already exists. | Change the existing article from `details.existing_id` (step 6). |
| `409 article_in_use` | The article still has stock and cannot be deleted. | Deactivate it instead of deleting it. |
| `422 invalid_article_value` | A value does not fit, e.g. a field that does not exist, no number in a number field, an unknown category or a missing `sku`. | The `message` names the field. Compare the slugs with step 2. |

!!!
Details the API does not know (e.g. a typo like `"attribute"` instead of `"attributes"`) are silently ignored when creating and changing. So after your first test, check in the response whether all values arrived.
!!!

You will find all errors and permissions at a glance in the [overview](overview.md#errors).
