# Ragently Custom Commerce Connector — Implementatiegids

> Dit document is **zelfstandig**: een externe ontwikkelaar of een AI-coding-assistent
> (GitHub Copilot, Claude Code, Cursor) kan er zonder verdere context een backend mee
> bouwen die voldoet aan het Ragently Custom Commerce-contract. Alle relevante
> endpoints, schema's, authenticatiemethodes en conventies staan hieronder volledig
> uitgeschreven.
>
> De machine-leesbare bron staat naast dit bestand: [`/custom-commerce-openapi.yaml`](./custom-commerce-openapi.yaml).
> Bij twijfel is die OpenAPI-spec leidend; dit document spiegelt hem 1-op-1.

---

## Instructions for AI Coding Assistants

**Lees dit blok eerst en volg het letterlijk.** Als je een AI-coding-assistent bent die
dit document gebruikt om de backend van de klant te implementeren, dan MOET je:

- **eerst de EIGEN codebase/backend/ERP/OMS van de klant analyseren** voordat je iets
  schrijft — begrijp hoe orders, klanten, producten, voorraad en authenticatie daar nu
  al werken;
- **de bestaande order-/klant-/product-/auth-logica van de klant hergebruiken** in
  plaats van parallelle logica op te tuigen;
- **bestaande functionaliteit niet breken** — geen regressies introduceren in wat de
  klant al draait;
- **geen nieuwe business-logica introduceren tenzij strikt noodzakelijk** voor het
  vervullen van dit contract;
- **bestaande services UITBREIDEN in plaats van dupliceren** — sluit aan op de al
  aanwezige repositories, service-lagen en data-toegangspatronen;
- **de bestaande architectuur, codeerstandaarden, naamgeving, validatie en
  foutafhandeling van de klant volgen** — jouw code moet niet te onderscheiden zijn van
  de rest van de codebase;
- **veilige authenticatie toepassen en alle input valideren** (zie *Security & Headers*);
- **productieklare code opleveren** — geen prototypes, geen `TODO`-gaten in het
  request-pad;
- **tests toevoegen waar de codebase van de klant al een teststructuur heeft** — volg
  het bestaande test-framework en de bestaande conventies;
- **de data van de klant EXACT mappen op de canonieke response-shapes van Ragently**
  zoals hieronder gespecificeerd — verzin **geen** eigen veldnamen, wijzig geen casing,
  voeg geen synoniemen toe waar een vaste key verwacht wordt;
- **alleen de endpoints implementeren die horen bij de capabilities die de backend in
  zijn manifest declareert** — niets meer, niets minder.

**Belangrijk om te begrijpen:** dit is het contract dat **de backend van de KLANT
implementeert**. Ragently is de **client** en roept deze endpoints **server-to-server**
aan. Dit is dus **geen** API die Ragently aanbiedt en die jij consumeert — het is
andersom. Jij bouwt de server; Ragently belt jou.

---

## 1. Introductie

**Ragently** is een AI-chatbot voor webshops. De bot beantwoordt klantvragen (orderstatus,
retouren, refunds, productadvies, voorraad, checkout-hulp) en voert namens de klant
acties uit tegen het webshopplatform. Voor Shopify, Lightspeed, WooCommerce en Wix heeft
Ragently kant-en-klare connectoren. De **Custom Commerce Connector** is de vijfde optie:
draait de webshop op een eigen/maatwerk-backend, ERP of OMS, dan implementeer je zelf
een klein aantal HTTP-endpoints volgens dit contract, en spreekt Ragently jouw systeem
aan alsof het een native platform is.

**Doel van de integratie:** jouw backend vertaalt de canonieke Ragently-datamodellen
(orders, klanten, producten, checkouts, kennisbank-content) naar jouw eigen dataopslag,
zodat de AI-chatbot dezelfde functionaliteit krijgt als bij een native platform —
zonder dat Ragently iets hoeft te weten over jouw interne systemen.

Je hoeft **niet** alles te bouwen. Via een **manifest** declareer je welke *capabilities*
(features) je ondersteunt, en je implementeert uitsluitend de endpoints die daarbij
horen.

---

## 2. Architectuur

```
┌────────────┐      ┌──────────────┐      ┌─────────────────────┐      ┌──────────────┐
│  Browser   │      │  Ragently    │      │  Custom Commerce     │      │  JOUW        │
│  (widget)  │─────▶│  API         │─────▶│  Connector           │─────▶│  backend     │─────▶ ERP / DB / OMS
│            │◀─────│  (chatbot)   │◀─────│  (custom_adapter.py) │◀─────│  (dit contract)│
└────────────┘      └──────────────┘      └─────────────────────┘      └──────────────┘
     HTTPS                 server-to-server (met auth-header, zie §3)
```

Kernpunten:

- De **browser/widget praat uitsluitend met de Ragently API.** De bezoeker laadt de
  chatwidget; die stuurt berichten naar Ragently.
- Ragently's **Custom Commerce Connector** (`custom_adapter.py` + `custom_client.py` in
  de Ragently-codebase) roept **server-to-server** jouw backend aan wanneer de bot
  order-/klant-/product-informatie nodig heeft of een actie moet uitvoeren.
- **Jouw backend is nooit rechtstreeks door de browser bereikbaar.** Er lopen geen
  credentials, tokens of endpoints van jou naar de client-side; alle verkeer met jou
  komt van Ragently's servers.
- Jouw backend antwoordt met de **canonieke JSON-shapes** uit dit document. De connector
  geeft die vrijwel ongewijzigd door aan de tool-laag en de widget; daarom zijn de
  exacte veldnamen bindend.

---

## 3. Authenticatie

Ragently authenticeert **zichzelf** bij jouw backend met precies **één** van de vier
methodes hieronder. Je kiest de methode bij het koppelen in het Ragently-dashboard en
vult de bijbehorende velden in. Ragently stuurt de gekozen credential mee op **elk**
verzoek (inclusief het manifest-verzoek).

| Methode  | Wat Ragently meestuurt | Wat jij in het dashboard invult |
|----------|------------------------|---------------------------------|
| **bearer** | `Authorization: Bearer <token>` op elk verzoek | `token` |
| **api_key** | Jouw API-key in een header. Standaard `X-API-Key: <key>`; je kunt een andere header-naam opgeven | `api_key` (+ optioneel `header_name`) |
| **basic** | HTTP Basic Authentication (`Authorization: Basic base64(username:password)`) | `username`, `password` |
| **oauth2** | Ragently haalt zelf een access-token op via de **client-credentials-grant** bij jouw `token_url`, en stuurt dat vervolgens mee als `Authorization: Bearer <access_token>`. Het token wordt gecachet en op tijd ververst (60s marge vóór expiry). | `token_url`, `client_id`, `client_secret` |

**Verplichte velden per methode** (ontbreekt er één, dan wijst het dashboard de koppeling
af):

- `bearer` → `token`
- `api_key` → `api_key`
- `basic` → `username`, `password`
- `oauth2` → `token_url`, `client_id`, `client_secret`

**Bij het koppelen** voert de merchant in het Ragently-dashboard in: de **`base_url`** van
je backend en één auth-methode met bijbehorende velden. Ragently valideert de URL (o.a.
tegen SSRF), haalt het manifest op, filtert de gedeclareerde capabilities op de bekende
constanten, en doet één lichte leesprobe (bij voorkeur `GET /shop`, anders een lichte read
per capability) om de koppeling te testen. Pas als dat slaagt wordt de koppeling opgeslagen.
De credentials worden **versleuteld** opgeslagen bij Ragently.

---

## 4. Security & Headers

- **HTTPS verplicht.** Alle endpoints moeten over TLS bereikbaar zijn. Ragently belt geen
  plain-HTTP-backends in productie.
- **Secrets versleuteld opgeslagen bij Ragently, nooit in de browser.** De auth-config
  bevat de host van je `base_url` in statusweergaven, maar nooit het pad, de query of de
  auth-secrets. Jouw endpoints zijn nooit client-side zichtbaar.
- **404-conventie (zie ook §7).** Bestaat een resource niet, geef dan **HTTP 404** terug.
  Voor GET-endpoints wordt de body dan genegeerd — alleen de statuscode telt. Voor
  schrijf-endpoints (POST/PUT) betekent 404 hetzelfde (resource niet gevonden) en wordt de
  body eveneens genegeerd; de connector zet dat intern om naar
  `{"success": false, "error": "not_found"}`.
- **Least privilege via capabilities.** Ragently roept uitsluitend de endpoints aan die
  horen bij de capabilities die jouw manifest declareert (plus hun interne
  afhankelijkheden, zie §5). Endpoints van niet-gedeclareerde capabilities worden nooit
  aangeroepen — je kunt ze weglaten.
- **Valideer alle input.** Query-params, path-params en request-bodies komen van buiten;
  behandel ze als onvertrouwd. Volg de bestaande validatie- en sanitatie-patronen van de
  klant.
- **Correcte statuscodes.** Alle 2xx-responses moeten `Content-Type: application/json`
  zijn. Gebruik 404 uitsluitend voor "resource niet gevonden". Elke andere niet-2xx-code
  (4xx/5xx) wordt door de connector geïnterpreteerd als een fout; geef bij voorkeur een
  JSON-body met een `error`-veld mee (zie het `Error`-schema in §9). De exacte inhoud
  wordt buiten logging/foutmeldingen niet machinaal geparsed.

---

## 5. Business Rules

**Het manifest is de enige bron van waarheid voor exposure.** Ragently roept precies de
endpoints aan die horen bij de capabilities die jij declareert. **Ragently verbreedt jouw
gedeclareerde set NOOIT zelf** — declareer je alleen `orders.cancel`, dan krijg je geen
`orders.read`-tools (orderstatus opzoeken, etc.) er gratis bij aan de agent-kant. Onbekende
of niet-serveerbare capability-strings worden stilzwijgend weggefilterd.

**Capability-afhankelijkheden (dependency rules).** Sommige acties resolven intern eerst
een resource via een *afhankelijke* capability. Je hoeft die afhankelijkheid niet apart in
je manifest te declareren, maar je **moet de onderliggende endpoints wél implementeren** als
je de afhankelijke capability declareert:

| Als je declareert… | …dan MOET je ook implementeren (reden) |
|--------------------|-----------------------------------------|
| `orders.cancel`, `orders.edit`, `orders.shipping_address.update`, `orders.notes.write`, `refunds.create`, `refunds.read`, `returns.create`, `returns.read` | **`orders.read`**: `GET /orders/find` + `GET /orders/{order_id}` — elke order-actie resolvet eerst de order-id via `find_order` |
| `products.metafields.read` | **`products.read`**: `GET /products/search` — metafields resolven eerst het product-id |
| `customers.write`, `customers.addresses.write`, `customers.marketing_consent.write` | **`customers.read`**: `GET /customers/by-phone` + `GET /customers/{customer_id}/orders` |
| `draft_orders.create` | **`products.read`**: `GET /products/search` — variant-naamresolutie voor de draft-order-regels |

Concreet: declareer je `refunds.create`, dan implementeer je naast de refund-endpoints ook
`GET /orders/find` en `GET /orders/{order_id}` (uit `orders.read`), zelfs als je
`orders.read` zelf niet als tool naar de agent wilt exposen.

**Identiteitsverificatie-model.** Ragently verifieert een klant **zelf** (aan zijn kant)
tegen een order/e-mailadres voordat het order- of klant-tools aanroept. **Jouw endpoints
hoeven geen sessie- of eigenaarschapscontrole te doen** — ze antwoorden gewoon eerlijk op
de vraag die gesteld wordt (geef de order/klant terug, of 404 als die niet bestaat). De
autorisatie "mag deze chatgebruiker deze order zien" ligt bij Ragently, niet bij jou.

**Volledige lijsten, geen dubbele paginering.** Waar het contract een volledige lijst
verwacht (bv. `GET /orders` per e-mailadres), lever je die in één response — de adapter
doet daar zelf geen vervolgaanroepen op. Waar het contract expliciet pagineert
(`/abandoned-checkouts`, `/catalog/products`), roept de connector herhaald aan met
oplopende `page` totdat je een **lege array** teruggeeft.

---

## 6. Manifest

Elke koppeling begint hier. Dit endpoint is **altijd verplicht**, ongeacht welke overige
capabilities je declareert.

### `GET /.well-known/ragently` — capability-manifest

Wordt aangeroepen zodra een merchant zijn backend-URL + auth-gegevens invoert (en opnieuw
bij elke "verbinding testen"), vóór elke andere aanroep. Antwoord met welke capabilities je
ondersteunt.

**Response `200` (`Manifest`):**

```json
{
  "version": "1.0.0",
  "capabilities": [
    "platform.info",
    "orders.read",
    "products.read",
    "knowledge.read"
  ]
}
```

- `version` (string, verplicht) — vrije versie-string van jouw manifest/implementatie.
- `capabilities` (array van string, verplicht) — de gedeclareerde capabilities. Alleen
  waarden uit de lijst hieronder worden herkend; de rest wordt weggefilterd. Als er na
  filtering geen enkele bekende capability overblijft, wordt de koppeling afgewezen.

**De 20 declareerbare capabilities** (dit is de volledige lijst; `products.native_recommendations`
en de `billing.*`-capabilities zijn **niet** declareerbaar door een custom backend):

| Capability | Betekenis |
|------------|-----------|
| `orders.read` | Orderstatus, orderhistorie en track & trace opvragen. |
| `orders.cancel` | Een order annuleren. |
| `orders.edit` | Regels (variants) toevoegen aan een bestaande, nog niet verzonden order. |
| `orders.shipping_address.update` | Het verzendadres van een order wijzigen. |
| `orders.notes.write` | De interne notitie/opmerking van een order bijwerken. |
| `refunds.create` | Een refund voorbereiden (preview) en boeken, incl. terugbetaalbare regels opvragen. |
| `refunds.read` | Reeds geboekte refunds van een order opvragen. |
| `returns.create` | Een retour (RMA) aanmaken en goedkeuren, incl. retourneerbare regels opvragen. |
| `returns.read` | Retourstatus (RMA's) van een order opvragen. |
| `customers.read` | Klantgegevens en klant-orderhistorie opvragen, klant zoeken op telefoonnummer. |
| `customers.write` | Basisgegevens van een klant (e-mail, telefoon, naam) wijzigen. |
| `customers.addresses.write` | Een klantadres aanmaken, wijzigen of als standaard instellen. |
| `customers.marketing_consent.write` | De e-mailmarketingtoestemming van een klant wijzigen. |
| `products.read` | Producten zoeken, productdetails opvragen, voorraad opvragen, varianten/producten per ID opvragen. |
| `products.metafields.read` | Aangepaste productmetadata (custom fields) opvragen. |
| `draft_orders.create` | Een conceptorder (draft order) met betaallink aanmaken (chat-checkout). |
| `checkouts.read` | Verlaten checkouts (abandoned checkouts) opvragen, voor herstel-flows. |
| `knowledge.read` | Beleidsdocumenten, pagina's en artikelen opvragen voor de kennisbank-sync. |
| `catalog.sync` | De volledige actieve productcatalogus itereren voor de RAG-index. |
| `platform.info` | Basisgegevens van de winkel opvragen; dient ook als verbindingstest. |

---

## 7. Alle endpoints

Hieronder staat **elke** operatie uit het contract, gegroepeerd per capability. Voor elk
endpoint: capability-tag, HTTP-methode + pad, params, request-body (indien van toepassing),
success-response met de canonieke veldnamen, 404-gedrag en wanneer Ragently het aanroept.

De volledige schema-referentie (velddefinities) staat in §8. Alle response-shapes komen
letterlijk uit `base.py` (het canonieke commerce-contract van Ragently).

---

### 7.1 `platform.info`

#### `GET /shop` — winkelgegevens (verbindingstest)

Dit is (indien gedeclareerd) het **eerste** endpoint dat Ragently aanroept om een nieuwe
koppeling te testen. Implementeer het als een lichte, snelle, altijd-beschikbare read.

- **Params:** geen.
- **Response `200` (`ShopInfo`):**

```json
{
  "id": "shop_123",
  "name": "Voorbeeldwinkel",
  "email": "hello@voorbeeldwinkel.nl",
  "domain": "voorbeeldwinkel.nl",
  "plan_name": "pro"
}
```

- Verplicht: `id`, `name`. Optioneel/nullable: `email`, `domain`, `plan_name`.
- **Wanneer:** bij het koppelen en bij elke verbindingstest; verder als de bot
  shopcontext nodig heeft.

---

### 7.2 `orders.read`

#### `GET /orders/find` — order zoeken op vrije identifier

Zoekt een order op ordernummer / `#naam` / GID / numeriek ID / vrije zoekterm. Eerste stap
van vrijwel elke order-, refund- en return-tool (resolvet de menselijke identifier naar een
interne order-id).

- **Query:** `identifier` (string, verplicht) — ordernummer, `#naam`, GID of vrije zoekterm.
- **Response `200`:** [`OrderInfo`](#orderinfo) (zie §8).
- **404:** geen order gevonden (body genegeerd).
- **Wanneer:** als de klant naar een order vraagt, en intern vóór elke order-actie.

#### `GET /orders/{order_id}` — order op ID (inclusief tracking)

Pure by-id-variant van `find_order`; **dezelfde `OrderInfo`-shape**. Wordt ook gebruikt voor
de tracking-weergave, dus `fulfillments[].trackingCompany/trackingNumber/trackingUrl` moeten
hier al gevuld zijn als de order fulfillments heeft.

- **Path:** `order_id` (string) — de interne order-id (zoals teruggegeven door `find_order.id`).
- **Response `200`:** [`OrderInfo`](#orderinfo).
- **404:** order niet gevonden (body genegeerd).
- **Wanneer:** orderdetails/tracking tonen nadat de order-id bekend is.

#### `GET /orders` — alle orders van een klant op e-mailadres

Levert de **volledige** (reeds samengevoegde) lijst orders van deze klant in één response —
de adapter pagineert hier niet.

- **Query:** `email` (string, verplicht, `format: email`); `page_size` (integer, optioneel,
  default 50) — hint voor het maximum aantal orders.
- **Response `200`:** array van [`OrderInfo`](#orderinfo) (mag leeg zijn).
- **Wanneer:** "toon mijn bestellingen" op basis van het geverifieerde e-mailadres.

---

### 7.3 `customers.read`

#### `GET /customers/{customer_id}/orders` — klant met diens recente orders

Klantnode met diens recente orders, voor klant-order-historieweergaven.

- **Path:** `customer_id` (string).
- **Query:** `first` (integer, optioneel, default 20) — maximumaantal orders.
- **Response `200`:** [`CustomerOrderHistory`](#customerorderhistory) — een object dat
  minstens een lijst orders bevat (bv. onder `orders`).
- **404:** klant niet gevonden (body genegeerd).
- **Wanneer:** klant-orderhistorie tonen.

#### `GET /customers/by-phone` — klant zoeken op telefoonnummer

Retourneert **uitsluitend** een klant bij **precies één** match; bij nul of meerdere matches
geef je **404**.

- **Query:** `phone` (string, verplicht).
- **Response `200`:** [`Customer`](#customer).
- **404:** geen of meer dan één klant gevonden (body genegeerd).
- **Wanneer:** klant identificeren op telefoonnummer.

---

### 7.4 `orders.cancel`

#### `POST /orders/{order_id}/cancel` — order annuleren

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "reason": "customer",
  "notify_customer": true,
  "restock": true
}
```

- Alle velden optioneel: `reason` (string, default `"customer"`), `notify_customer`
  (boolean, default `true`), `restock` (boolean, default `true`).
- **Response `200` (`CancelOrderResult`):**

```json
{ "success": true, "job": { } }
```

- Verplicht: `success` (boolean). Optioneel: `job` (object, platformafhankelijk, bv. een
  async-job-referentie), `error` (string).
- **404:** order niet gevonden — connector maakt er intern
  `{"success": false, "error": "not_found"}` van.
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.5 `orders.edit`

#### `POST /orders/{order_id}/line-items` — regel toevoegen aan een bestaande order

Alleen bedoeld voor **volledig onverzonden** orders. De tool-laag van Ragently controleert
dit al vóór de aanroep, maar je mag het zelf ook afdwingen en met een foutresultaat
antwoorden als de order al (deels) verzonden is.

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "variant_id": "variant_789",
  "quantity": 1,
  "notify_customer": true,
  "staff_note": "Toegevoegd via chat"
}
```

- Verplicht: `variant_id` (string), `quantity` (integer ≥ 1). Optioneel: `notify_customer`
  (boolean, default `true`), `staff_note` (string, nullable).
- **Response `200` (`OrderEditResult`):** `{ "success": true }` — verplicht `success`
  (boolean), optioneel `error` (string) plus vrije extra velden.
- **404:** order niet gevonden → `{"success": false, "error": "not_found"}`.
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.6 `orders.shipping_address.update`

#### `PUT /orders/{order_id}/shipping-address` — verzendadres wijzigen

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "address": {
    "address1": "Hoofdstraat 1",
    "address2": "",
    "city": "Amsterdam",
    "province": "Noord-Holland",
    "zip": "1011AA",
    "country": "Netherlands",
    "phone": "+31201234567"
  }
}
```

- Verplicht: `address` ([`Address`](#address)).
- **Response `200` (`UpdateShippingAddressResult`):**

```json
{ "success": true, "order": { } }
```

- Verplicht: `success` (boolean). Optioneel: `order` ([`OrderInfo`](#orderinfo), nullable),
  `error` (string).
- **404:** order niet gevonden → `{"success": false, "error": "not_found"}`.
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.7 `orders.notes.write`

#### `POST /orders/{order_id}/notes` — ordernotitie zetten/overschrijven

- **Path:** `order_id` (string).
- **Request-body:** `{ "note": "..." }` — verplicht `note` (string).
- **Response `200` (`SetOrderNoteResult`):**

```json
{ "success": true, "note": "Klant belde over levering", "errors": [] }
```

- Verplicht: `success` (boolean). Optioneel: `note` (string, nullable), `errors`
  (array van string), `error` (string).
- **404:** order niet gevonden → `{"success": false, "error": "not_found"}`.
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.8 `refunds.read`

#### `GET /orders/{order_id}/refunds` — reeds geboekte refunds van een order

- **Path:** `order_id` (string).
- **Response `200`:** array van [`RefundListItem`](#refundlistitem) (mag leeg zijn):

```json
[
  {
    "refund_id": "refund_1",
    "created_at": "2026-07-01T10:00:00Z",
    "amount": 24.95,
    "currency": "EUR",
    "note": "Kapotte verpakking",
    "items": [ { "name": "T-shirt maat M", "quantity": 1 } ]
  }
]
```

- **404:** order niet gevonden (body genegeerd).
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.9 `refunds.create`

Deze capability bundelt vier operaties: refund-preview, refund boeken, en het opvragen van
terugbetaalbare regels.

#### `GET /orders/{order_id}/refundable-items` — terugbetaalbare regels

Voedt de server-side naamresolutie van `create_refund`: de AI noemt producten bij naam,
Ragently matcht dat tegen deze lijst om het juiste `id` (lineItemId) en de maximale
`refundableQuantity` te bepalen. **Sleutelnamen zijn bewust ongewijzigd** t.o.v. de rauwe
node.

- **Path:** `order_id` (string).
- **Response `200`:** array van [`RefundableLineItem`](#refundablelineitem):

```json
[
  { "id": "lineitem_1", "name": "T-shirt maat M", "quantity": 2, "refundableQuantity": 2 }
]
```

- Verplicht per item: `id`, `name`, `quantity`, `refundableQuantity`. Het `id` is het
  **LineItem-id** dat `create_refund` verwacht in `refund_line_items[].lineItemId`.
- **404:** order niet gevonden (body genegeerd).

#### `POST /orders/{order_id}/refund/suggest` — refund-preview (zonder te boeken)

Levert een preview van bedragen/transacties, zonder iets te boeken. Wordt o.a. gebruikt om
een geconfigureerd maximumbedrag (`refund_max_amount`) te toetsen vóórdat `create_refund`
draait.

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "refund_line_items": [ { "lineItemId": "lineitem_1", "quantity": 1, "restockType": "NO_RESTOCK" } ],
  "shipping_amount": 0
}
```

- Verplicht: `refund_line_items` (array van [`RefundLineItemInput`](#refundlineiteminput)).
  Optioneel: `shipping_amount` (number, nullable).
- **Response `200` (`SuggestedRefund`):** de connector leest in elk geval
  `suggestedTransactions[].amount` (som daarvan):

```json
{ "suggestedTransactions": [ { "amount": 12.5 } ] }
```

- **404:** order niet gevonden (body genegeerd).

#### `POST /orders/{order_id}/refunds` — refund boeken

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "refund_line_items": [ { "lineItemId": "lineitem_1", "quantity": 1, "restockType": "NO_RESTOCK" } ],
  "shipping_refund": 0,
  "notify_customer": true,
  "note": "Coulance"
}
```

- Verplicht: `refund_line_items` (array van [`RefundLineItemInput`](#refundlineiteminput)).
  Optioneel: `shipping_refund` (number, nullable), `notify_customer` (boolean, default
  `true`), `note` (string, nullable).
- **Response `200` (`RefundResult`):**

```json
{ "success": true, "refund_id": "refund_2", "amount": 12.5, "currency": "EUR" }
```

- Verplicht: `success` (boolean). Optioneel: `refund_id`, `amount`, `currency`, `error`.
- **404:** order niet gevonden → `{"success": false, "error": "not_found"}`.

**Afhankelijkheid (hele `refunds.create`):** vereist ook `orders.read`.

---

### 7.10 `returns.read`

#### `GET /orders/{order_id}/returns` — retouren (RMA's) van een order

- **Path:** `order_id` (string).
- **Response `200`:** array van [`ReturnListItem`](#returnlistitem) (mag leeg zijn):

```json
[
  {
    "return_id": "return_1",
    "status": "REQUESTED",
    "created_at": "2026-07-02T09:00:00Z",
    "items": [ { "name": "Broek maat 32", "quantity": 1 } ]
  }
]
```

- Verplicht per item: `return_id`, `status`.
- **404:** order niet gevonden (body genegeerd).
- **Afhankelijkheid:** vereist ook `orders.read`.

---

### 7.11 `returns.create`

Bundelt drie operaties: retourneerbare regels opvragen, retour starten, retour goedkeuren.

#### `GET /orders/{order_id}/returnable-items` — retourneerbare regels

Voedt de server-side naamresolutie van `create_return`. **LET OP:** het `id`-veld leeft in
een **ANDERE id-ruimte** dan `refundable-items[].id`. Dit moet het id zijn dat
`create_return` verwacht in `return_line_items[].fulfillmentLineItemId` (bij Shopify bijv.
een FulfillmentLineItem-GID, geen LineItem-GID). `quantity` is de **resterende**
retourneerbare hoeveelheid (na aftrek van eerdere retouren); regels zonder resterende
hoeveelheid horen niet in de lijst.

- **Path:** `order_id` (string).
- **Response `200`:** array van [`ReturnableLineItem`](#returnablelineitem):

```json
[
  { "id": "fulfillmentlineitem_1", "name": "Broek maat 32", "quantity": 1 }
]
```

- Verplicht per item: `id`, `name`, `quantity`.
- **404:** order niet gevonden (body genegeerd).

#### `POST /orders/{order_id}/returns` — retour (RMA) starten

- **Path:** `order_id` (string).
- **Request-body:**

```json
{
  "return_line_items": [ { "fulfillmentLineItemId": "fulfillmentlineitem_1", "quantity": 1 } ],
  "notify_customer": true
}
```

- Verplicht: `return_line_items` (array van [`ReturnLineItemInput`](#returnlineiteminput)).
  Optioneel: `notify_customer` (boolean, default `true`).
- **Response `200` (`ReturnResult`):** `{ "success": true }` — verplicht `success`
  (boolean), optioneel `error` (string) plus vrije extra velden.
- **404:** order niet gevonden → `{"success": false, "error": "not_found"}`.

#### `POST /returns/{return_id}/approve` — aangevraagde retour goedkeuren

- **Path:** `return_id` (string).
- **Request-body:** geen.
- **Response `200` (`ApproveReturnResult`):** `{ "success": true }` — verplicht `success`
  (boolean), optioneel `error` (string).
- **404:** retour niet gevonden → `{"success": false, "error": "not_found"}`.

**Afhankelijkheid (hele `returns.create`):** vereist ook `orders.read`.

---

### 7.12 `customers.write`

#### `PUT /customers/{customer_id}` — basisgegevens van een klant wijzigen

- **Path:** `customer_id` (string).
- **Request-body** (alle velden optioneel/nullable):

```json
{ "email": "nieuw@voorbeeld.nl", "phone": "+31612345678", "first_name": "Jan", "last_name": "Jansen" }
```

- **Response `200` (`CustomerUpdateResult`):**

```json
{ "success": true, "customer": { "id": "cust_1", "email": "nieuw@voorbeeld.nl" } }
```

- Verplicht: `success` (boolean). Optioneel: `customer` ([`Customer`](#customer), nullable),
  `error` (string).
- **404:** klant niet gevonden → `{"success": false, "error": "not_found"}`.
- **Afhankelijkheid:** vereist ook `customers.read`.

---

### 7.13 `customers.addresses.write`

Alle drie de operaties delen het [`CustomerAddressResult`](#customeraddressresult)-schema.

#### `POST /customers/{customer_id}/addresses` — klantadres toevoegen

- **Path:** `customer_id` (string).
- **Request-body:** verplicht `address` ([`Address`](#address)); optioneel `set_as_default`
  (boolean, default `false`).
- **Response `200`:** [`CustomerAddressResult`](#customeraddressresult).
- **404:** klant niet gevonden → `{"success": false, "error": "not_found"}`.

#### `PUT /customers/{customer_id}/addresses/{address_id}` — bestaand klantadres wijzigen

- **Path:** `customer_id`, `address_id` (string).
- **Request-body:** verplicht `address` ([`Address`](#address)); optioneel `set_as_default`
  (boolean, default `false`).
- **Response `200`:** [`CustomerAddressResult`](#customeraddressresult).
- **404:** klant of adres niet gevonden → `{"success": false, "error": "not_found"}`.

#### `POST /customers/{customer_id}/addresses/{address_id}/default` — adres als standaard instellen

- **Path:** `customer_id`, `address_id` (string).
- **Request-body:** geen.
- **Response `200`:** [`CustomerAddressResult`](#customeraddressresult).
- **404:** klant of adres niet gevonden → `{"success": false, "error": "not_found"}`.

**Afhankelijkheid (hele `customers.addresses.write`):** vereist ook `customers.read`.

---

### 7.14 `customers.marketing_consent.write`

#### `PUT /customers/{customer_id}/marketing-consent` — e-mailmarketingtoestemming wijzigen

- **Path:** `customer_id` (string).
- **Request-body:** verplicht `marketing_state` (string); optioneel `opt_in_level`
  (string, nullable).

```json
{ "marketing_state": "SUBSCRIBED", "opt_in_level": "SINGLE_OPT_IN" }
```

- **Response `200` (`MarketingConsentResult`):**

```json
{ "success": true, "marketing_state": "SUBSCRIBED", "errors": [] }
```

- Verplicht: `success` (boolean). Optioneel: `marketing_state` (string, nullable), `errors`
  (array van string), `error` (string).
- **404:** klant niet gevonden → `{"success": false, "error": "not_found"}`.
- **Afhankelijkheid:** vereist ook `customers.read`.

---

### 7.15 `products.read`

Deze capability bedient vijf interne aanroepvormen. Let op: `GET /products/search` bedient
er alleen al vier, onderscheiden door query-params.

#### `GET /products/search` — producten zoeken (met optionele details/voorraad)

Eén endpoint, vier vormen:

- **Zonder `details`/`stock`** — vrije productzoekopdracht → items volgens
  [`ProductSearchItem`](#productsearchitem).
- **`details=1`** — rijke productdetails → items volgens [`ProductDetailItem`](#productdetailitem).
- **`stock=1`** — voorraadoverzicht → items volgens [`ProductStockItem`](#productstockitem).
- **`limit=1` zonder verdere vlaggen** — wordt ook gebruikt om één product-id te resolven
  uit `results[0].product_id` (`resolve_product_id`).

Implementeer minimaal de basisvorm (`ProductSearchItem`, **inclusief `product_id`** — dat
veld is vereist voor `resolve_product_id`). De `details`/`stock`-vormen zijn nodig zodra je
de bijbehorende sub-functies wilt ondersteunen; ze delen dezelfde capability en worden dus
altijd samen aangeboden zodra `products.read` gedeclareerd is.

- **Query:** `q` (string, verplicht — mag leeg zijn); `limit` (integer, optioneel, default 5);
  `details` (`"1"`, optioneel); `stock` (`"1"`, optioneel).
- **Response `200`:** array van items in de vorm die bij de vlaggen hoort (mag leeg zijn).

Voorbeeld basisvorm:

```json
[
  {
    "product_id": "prod_1",
    "product_title": "Basic T-shirt",
    "variant_title": "Maat M",
    "variant_id": "variant_1",
    "variantId": "variant_1",
    "price": 24.95,
    "available": true,
    "image_url": "https://cdn.example.com/tshirt.jpg"
  }
]
```

#### `POST /products/variants` — varianten opvragen per ID

- **Request-body:** `{ "variant_ids": ["variant_1", "variant_2"] }` — verplicht
  `variant_ids` (array van string).
- **Response `200` (`VariantsByIdsResult`):** dict per variant-id → [`VariantInfo`](#variantinfo)
  (mag leeg zijn):

```json
{
  "variant_1": { "variant_title": "Maat M", "price": 24.95, "available": true, "image_url": "https://cdn.example.com/tshirt.jpg" }
}
```

#### `POST /products/by-ids` — producten opvragen per ID

- **Request-body:** `{ "product_ids": ["prod_1"] }` — verplicht `product_ids` (array van string).
- **Response `200` (`ProductsByIdsResult`):** dict per product-id → [`ProductByIdInfo`](#productbyidinfo)
  (mag leeg zijn):

```json
{
  "prod_1": { "title": "Basic T-shirt", "handle": "basic-t-shirt", "image_url": "https://cdn.example.com/tshirt.jpg" }
}
```

**Wanneer (hele `products.read`):** productvragen, voorraadvragen, productdetails, en interne
variant-/product-id-resolutie voor draft-orders en metafields.

---

### 7.16 `products.metafields.read`

#### `GET /products/{product_id}/metafields` — productmetafields opvragen

- **Path:** `product_id` (string).
- **Query:** `namespace` (string, optioneel, nullable); `first` (integer, optioneel, default 50).
- **Response `200` (`ProductMetafields`):**

```json
{
  "product_title": "Basic T-shirt",
  "handle": "basic-t-shirt",
  "metafields": [
    { "namespace": "specs", "key": "materiaal", "type": "single_line_text_field", "value": "100% katoen" }
  ]
}
```

- Verplicht: `metafields` (array van [`Metafield`](#metafield)). Optioneel: `product_title`,
  `handle`.
- **404:** product niet gevonden (body genegeerd).
- **Afhankelijkheid:** vereist ook `products.read`.

---

### 7.17 `draft_orders.create`

#### `POST /draft-orders` — conceptorder met betaallink aanmaken

- **Request-body:**

```json
{
  "line_items": [ { "variantId": "variant_1", "quantity": 2 } ],
  "email": "klant@voorbeeld.nl",
  "note": "Via chat besteld",
  "note_attributes": [ { "name": "bron", "value": "chatbot" } ],
  "order_discount": null,
  "free_shipping": false,
  "tags": ["chat-checkout"]
}
```

- Verplicht: `line_items` (array van [`DraftOrderLineItemInput`](#draftorderlineiteminput);
  `variantId` is server-side al geresolved uit de door de AI genoemde producttitel).
  Optioneel: `email` (nullable), `note` (nullable), `note_attributes` (array van
  `{name, value}`, nullable), `order_discount` (object, platformafhankelijk, nullable —
  laat leeg als je geen orderkorting ondersteunt), `free_shipping` (boolean, default
  `false`), `tags` (array van string, nullable).
- **Response `200` (`DraftOrderResult`):**

```json
{
  "draft_order_id": "draft_1",
  "invoice_url": "https://pay.example.com/draft_1",
  "total_price": 49.9,
  "currency": "EUR",
  "status": "open"
}
```

- Verplicht: `draft_order_id` (string), `invoice_url` (string — de betaallink die aan de
  klant getoond wordt). Optioneel: `total_price`, `currency`, `status`.
- **Afhankelijkheid:** vereist ook `products.read` (variant-naamresolutie).

---

### 7.18 `checkouts.read`

#### `GET /abandoned-checkouts` — verlaten checkouts opvragen (gepagineerd)

Wordt herhaald aangeroepen met oplopende `page` totdat je een **lege lijst** teruggeeft (of
totdat de connector eerder stopt op eigen stopcriteria). Geef in elk item `customer.email`
mee — het e-mailfilter (matchen tegen de ingelogde klant) gebeurt aan Ragently's kant.

- **Query:** `page_size` (integer, optioneel, default 50); `page` (integer, optioneel,
  default 1 — **1-based**).
- **Response `200`:** array van [`AbandonedCheckout`](#abandonedcheckout) (lege array =
  einde van de paginering):

```json
[
  { "id": "checkout_1", "customer": { "email": "klant@voorbeeld.nl" } }
]
```

- Verplicht per item: `customer` met daarin verplicht `customer.email`.

---

### 7.19 `catalog.sync`

#### `GET /catalog/products` — volledige productcatalogus itereren (gepagineerd)

Wordt herhaald aangeroepen met oplopende `page` (startend bij 1) totdat je een **lege lijst**
teruggeeft — dat signaleert het einde van de sync. Gebruikt voor de RAG-productindex.

- **Query:** `page` (integer, verplicht, default 1 — **1-based**).
- **Response `200`:** array van [`CatalogProduct`](#catalogproduct) (lege array = einde):

```json
[
  {
    "product_id": "prod_1",
    "title": "Basic T-shirt",
    "description": "Zacht katoenen T-shirt.",
    "tags": ["shirt", "katoen"],
    "product_type": "Kleding",
    "vendor": "Voorbeeldmerk",
    "handle": "basic-t-shirt",
    "image_url": "https://cdn.example.com/tshirt.jpg",
    "variant_ids": ["variant_1", "variant_2"]
  }
]
```

- Verplicht per item: `product_id`, `title`. `description` is **plain text** (geen HTML).

---

### 7.20 `knowledge.read`

#### `GET /knowledge` — shopcontent voor de kennisbank

- **Params:** geen.
- **Response `200` (`KnowledgeDocuments`):** alleen de "kinds" die jouw platform heeft
  hoeven aanwezig te zijn. `body` is **HTML-inhoud**. `errors` is optioneel (map van kind →
  foutmelding, alleen voor kinds die mislukten).

```json
{
  "policies": [ { "title": "Retourbeleid", "body": "<p>Retourneren binnen 30 dagen…</p>" } ],
  "pages":    [ { "title": "Over ons", "body": "<p>Wij zijn…</p>" } ],
  "articles": [ { "title": "Wasadvies", "body": "<p>Was op 30 graden…</p>" } ],
  "errors": { }
}
```

- Elk item in `policies`/`pages`/`articles` is een [`KnowledgeArticleLike`](#knowledgearticlelike)
  met verplicht `title` en `body`.

---

## 8. Request/Response-modellen (schema-referentie)

Alle veldnamen hieronder zijn **bindend**. Objecten met "vrije extra velden toegestaan"
mappen op `additionalProperties: true` in de OpenAPI-spec — je mag daar extra keys aan
toevoegen, maar de gedocumenteerde keys moeten exact zo heten.

### Manifest
- `version` (string, **verplicht**)
- `capabilities` (array van string, **verplicht**; enum = de 20 capabilities uit §6)

### Error
- `error` (string) — aanbevolen foutvorm voor niet-2xx-responses (behalve 404, waarvan de
  body genegeerd wordt).

### <a id="address"></a>Address
Vrije extra velden toegestaan. Alle velden string:
- `address1`, `address2`, `city`, `province`, `zip`, `country`, `phone`

### <a id="customer"></a>Customer
Vrije extra velden toegestaan. De velden die de connector daadwerkelijk leest/schrijft:
- `id` (string)
- `email` (string, nullable), `phone` (string, nullable)
- `first_name` (string, nullable), `last_name` (string, nullable)

### LineItem
Regel binnen een order; buiten het onderstaande laat het contract de veldset los (vrije
extra velden toegestaan):
- `id` (string), `name` (string), `quantity` (integer)

### Fulfillment
Vorm platformafhankelijk (vrije extra velden toegestaan):
- `id` (string), `status` (string)
- `trackingCompany` (string, nullable), `trackingNumber` (string, nullable),
  `trackingUrl` (string, nullable)

### <a id="orderinfo"></a>OrderInfo
Canonieke, platformonafhankelijke order-shape (zelfde vorm ongeacht welk order-endpoint hem
teruggeeft). Vrije extra velden toegestaan.
- **Verplicht:** `id` (string), `name` (string — mens-leesbaar ordernummer, bv. `#1001`),
  `displayFinancialStatus` (string), `displayFulfillmentStatus` (string),
  `createdAt` (string, date-time)
- **Optioneel:** `legacyResourceId` (string, nullable), `email` (string, nullable),
  `phone` (string, nullable), `customer` ([`Customer`](#customer), nullable),
  `shippingAddress` ([`Address`](#address), nullable), `fulfillments` (array van
  [`Fulfillment`](#fulfillment)), `lineItems` (array van [`LineItem`](#lineitem))

### <a id="customerorderhistory"></a>CustomerOrderHistory
Object (vrije extra velden toegestaan). Losjes gedocumenteerd; verwacht minstens een lijst
orders (bv. onder `orders`).

### <a id="refundlistitem"></a>RefundListItem
- **Verplicht:** `refund_id` (string), `amount` (number), `currency` (string)
- **Optioneel:** `created_at` (string, date-time, nullable), `note` (string, nullable),
  `items` (array van `{name (string), quantity (integer)}` — RefundLineItemSummary)

### <a id="returnlistitem"></a>ReturnListItem
- **Verplicht:** `return_id` (string), `status` (string)
- **Optioneel:** `created_at` (string, date-time, nullable), `items` (array van
  `{name (string), quantity (integer)}` — ReturnLineItemSummary)

### <a id="refundablelineitem"></a>RefundableLineItem
Sleutelnamen bewust ongewijzigd t.o.v. de rauwe node.
- **Verplicht:** `id` (string — **LineItem-id**, verwacht in `refund_line_items[].lineItemId`),
  `name` (string), `quantity` (integer), `refundableQuantity` (integer)

### <a id="returnablelineitem"></a>ReturnableLineItem
- **Verplicht:** `id` (string — **FulfillmentLineItem-id**, ANDERE id-ruimte dan
  RefundableLineItem.id; verwacht in `return_line_items[].fulfillmentLineItemId`),
  `name` (string), `quantity` (integer — **resterende** retourneerbare hoeveelheid)

### <a id="refundlineiteminput"></a>RefundLineItemInput
Input voor `refund/suggest` en `refunds` (create_refund).
- **Verplicht:** `lineItemId` (string), `quantity` (integer ≥ 1)
- **Optioneel:** `restockType` (string, nullable — bv. `NO_RESTOCK`; platformafhankelijk,
  mag genegeerd worden)

### <a id="returnlineiteminput"></a>ReturnLineItemInput
Input voor `create_return`.
- **Verplicht:** `fulfillmentLineItemId` (string), `quantity` (integer ≥ 1)

### SuggestedRefund
Vrije extra velden toegestaan. De connector leest in elk geval:
- `suggestedTransactions` (array van `{amount (number), …}`) — de som van `amount` wordt
  getoetst tegen `refund_max_amount`.

### RefundResult
Vrije extra velden toegestaan.
- **Verplicht:** `success` (boolean)
- **Optioneel:** `refund_id` (string, nullable), `amount` (number, nullable),
  `currency` (string, nullable), `error` (string, nullable)

### ReturnResult
Vrije extra velden toegestaan. `success`/`error` is het enige harde contract.
- **Verplicht:** `success` (boolean) — **Optioneel:** `error` (string, nullable)

### ApproveReturnResult
- **Verplicht:** `success` (boolean) — **Optioneel:** `error` (string, nullable)

### SetOrderNoteResult
- **Verplicht:** `success` (boolean)
- **Optioneel:** `note` (string, nullable), `errors` (array van string), `error` (string, nullable)

### UpdateShippingAddressResult
- **Verplicht:** `success` (boolean)
- **Optioneel:** `order` ([`OrderInfo`](#orderinfo), nullable), `error` (string, nullable)

### CancelOrderResult
- **Verplicht:** `success` (boolean)
- **Optioneel:** `job` (object, platformafhankelijk, nullable), `error` (string, nullable)

### OrderEditResult
Vrije extra velden toegestaan.
- **Verplicht:** `success` (boolean) — **Optioneel:** `error` (string, nullable)

### CustomerUpdateResult
- **Verplicht:** `success` (boolean)
- **Optioneel:** `customer` ([`Customer`](#customer), nullable), `error` (string, nullable)

### <a id="customeraddressresult"></a>CustomerAddressResult
Gedeeld voor create/update/set-default van klantadressen. Vrije extra velden toegestaan.
- **Verplicht:** `success` (boolean)
- **Optioneel:** `address` ([`Address`](#address), nullable), `error` (string, nullable)

### MarketingConsentResult
- **Verplicht:** `success` (boolean)
- **Optioneel:** `marketing_state` (string, nullable), `errors` (array van string),
  `error` (string, nullable)

### <a id="productsearchitem"></a>ProductSearchItem
Standaardvorm van `GET /products/search`. Vrije extra velden toegestaan.
- `product_id` (string, nullable — **vereist voor `resolve_product_id`**; gelezen als
  `results[0].product_id`)
- `product_title` (string), `variant_title` (string, nullable)
- `variant_id` (string, nullable), `variantId` (string, nullable — **alias** van
  `variant_id`; sommige toolpaden lezen deze key)
- `price` (number, nullable), `available` (boolean, nullable), `image_url` (string, nullable)

### <a id="productdetailitem"></a>ProductDetailItem
Vorm van `GET /products/search?details=1`. Vrije extra velden toegestaan.
- `title` (string), `description` (string, nullable), `product_type` (string, nullable),
  `vendor` (string, nullable), `tags` (array van string)
- `variants` (array van **ProductDetailVariant**: `title` (string), `price` (number, nullable),
  `sku` (string, nullable), `available` (boolean, nullable), `stock` (integer, nullable))

### <a id="productstockitem"></a>ProductStockItem
Vorm van `GET /products/search?stock=1`. Vrije extra velden toegestaan.
- `title` (string)
- `variants` (array van **ProductStockVariant**: `variant` (string), `available` (boolean,
  nullable), `stock` (integer, nullable), `continues_selling_when_out_of_stock` (boolean,
  nullable))

### <a id="variantinfo"></a>VariantInfo
Waarde-object in `VariantsByIdsResult`. Vrije extra velden toegestaan.
- `variant_title` (string), `price` (number, nullable), `available` (boolean, nullable),
  `image_url` (string, nullable)

### VariantsByIdsResult
Dict: variant-id (string) → [`VariantInfo`](#variantinfo).

### <a id="productbyidinfo"></a>ProductByIdInfo
Waarde-object in `ProductsByIdsResult`. Vrije extra velden toegestaan (bv. varianten).
- `title` (string), `handle` (string, nullable), `image_url` (string, nullable)

### ProductsByIdsResult
Dict: product-id (string) → [`ProductByIdInfo`](#productbyidinfo).

### <a id="metafield"></a>Metafield
- **Verplicht:** `namespace` (string), `key` (string), `type` (string), `value` (string)

### ProductMetafields
- **Verplicht:** `metafields` (array van [`Metafield`](#metafield))
- **Optioneel:** `product_title` (string, nullable), `handle` (string, nullable)

### <a id="draftorderlineiteminput"></a>DraftOrderLineItemInput
- **Verplicht:** `variantId` (string — server-side al geresolved), `quantity` (integer ≥ 1)

### DraftOrderResult
Vrije extra velden toegestaan.
- **Verplicht:** `draft_order_id` (string), `invoice_url` (string — betaallink)
- **Optioneel:** `total_price` (number, nullable), `currency` (string, nullable),
  `status` (string, nullable)

### <a id="abandonedcheckout"></a>AbandonedCheckout
Rauwe node. Vrije extra velden toegestaan.
- **Verplicht:** `customer` (object met **verplicht** `customer.email` (string, email))
- **Optioneel:** `id` (string, nullable)

### <a id="catalogproduct"></a>CatalogProduct
Genormaliseerd, platformonafhankelijk catalogusitem.
- **Verplicht:** `product_id` (string), `title` (string)
- **Optioneel:** `description` (string — **plain text**), `tags` (array van string),
  `product_type` (string, nullable), `vendor` (string, nullable), `handle` (string, nullable),
  `image_url` (string, nullable), `variant_ids` (array van string)

### <a id="knowledgearticlelike"></a>KnowledgeArticleLike
- **Verplicht:** `title` (string), `body` (string — **HTML**)

### KnowledgeDocuments
- `policies`, `pages`, `articles` (elk array van [`KnowledgeArticleLike`](#knowledgearticlelike),
  alleen de kinds die je platform heeft)
- `errors` (object: kind → foutmelding, alleen voor mislukte kinds)

### <a id="shopinfo"></a>ShopInfo
Vrije extra velden toegestaan.
- **Verplicht:** `id` (string), `name` (string)
- **Optioneel:** `email` (string, nullable), `domain` (string, nullable),
  `plan_name` (string, nullable)

---

## 9. JSON-voorbeelden (kern-endpoints)

**Manifest — `GET /.well-known/ragently` → 200:**

```json
{ "version": "1.0.0", "capabilities": ["platform.info", "orders.read", "refunds.read", "refunds.create"] }
```

**Order zoeken — `GET /orders/find?identifier=%231001` → 200 (`OrderInfo`):**

```json
{
  "id": "order_1001",
  "legacyResourceId": "1001",
  "name": "#1001",
  "email": "klant@voorbeeld.nl",
  "phone": "+31612345678",
  "displayFinancialStatus": "PAID",
  "displayFulfillmentStatus": "FULFILLED",
  "createdAt": "2026-06-15T14:30:00Z",
  "customer": { "id": "cust_1", "email": "klant@voorbeeld.nl", "first_name": "Jan", "last_name": "Jansen" },
  "shippingAddress": { "address1": "Hoofdstraat 1", "city": "Amsterdam", "zip": "1011AA", "country": "Netherlands" },
  "fulfillments": [
    { "id": "ful_1", "status": "SUCCESS", "trackingCompany": "PostNL", "trackingNumber": "3STBJG123456789", "trackingUrl": "https://postnl.nl/track/3STBJG123456789" }
  ],
  "lineItems": [ { "id": "lineitem_1", "name": "Basic T-shirt maat M", "quantity": 2 } ]
}
```

**Producten zoeken — `GET /products/search?q=t-shirt&limit=5` → 200:**

```json
[
  {
    "product_id": "prod_1",
    "product_title": "Basic T-shirt",
    "variant_title": "Maat M",
    "variant_id": "variant_1",
    "variantId": "variant_1",
    "price": 24.95,
    "available": true,
    "image_url": "https://cdn.example.com/tshirt-m.jpg"
  }
]
```

**Refunds ophalen — `GET /orders/order_1001/refunds` → 200:**

```json
[
  {
    "refund_id": "refund_1",
    "created_at": "2026-06-20T09:00:00Z",
    "amount": 24.95,
    "currency": "EUR",
    "note": "Verkeerde maat",
    "items": [ { "name": "Basic T-shirt maat M", "quantity": 1 } ]
  }
]
```

**Refund boeken — `POST /orders/order_1001/refunds` → 200 (success):**

```json
{ "success": true, "refund_id": "refund_2", "amount": 12.5, "currency": "EUR" }
```

**Refund boeken — foutresultaat (2xx met `success: false`):**

```json
{ "success": false, "error": "refund_amount_exceeds_refundable" }
```

---

## 10. Error handling

Twee gescheiden mechanismen:

**1. 404 — resource niet gevonden.**
- **GET-endpoints:** geef HTTP 404, body wordt genegeerd (lege body is prima — alleen de
  statuscode telt). De connector behandelt dit als "niet gevonden" (bv. `find_order` →
  `None`, lijst-endpoints → lege lijst).
- **Schrijf-endpoints (POST/PUT):** 404 betekent hetzelfde, body wordt genegeerd. De
  connector zet dit intern om naar `{"success": false, "error": "not_found"}`, zodat callers
  die `result["success"]` lezen geen KeyError krijgen.

Voorbeeld — `GET /orders/find?identifier=onbekend`:

```
HTTP/1.1 404 Not Found
```

**2. Elke andere niet-2xx (4xx/5xx) — fout.**
De connector interpreteert dit als een fout. Geef bij voorkeur een JSON-body met een
`error`-veld mee (schema `Error`). De exacte inhoud wordt buiten logging/foutmeldingen niet
machinaal geparsed.

Voorbeeld — mislukte auth of interne fout:

```
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{ "error": "database_unavailable" }
```

**3. Business-fout binnen een geslaagde call (2xx, `success: false`).**
Voor schrijf-endpoints die een `…Result`-shape teruggeven mag een verwachte, niet-fatale
mislukking (bv. bedrag te hoog, order al verzonden) als een **2xx** met
`{"success": false, "error": "..."}` teruggegeven worden — reserveer niet-2xx-codes voor
echte transport-/serverfouten.

**Alle 2xx-responses moeten `application/json` zijn.**

---

## 11. Verwacht gedrag per endpoint — contractgaranties (samenvatting)

- **Idempotente reads.** Alle GET-endpoints zijn puur lezend; ze mogen herhaald worden
  aangeroepen (o.a. tijdens verbindingstests en paginering) zonder neveneffecten.
- **404 = niet gevonden, nooit "leeg is een fout".** Een lege lijst (voor lijst-endpoints)
  is een geldige 200-respons, geen 404.
- **Paginering stopt op lege array.** `GET /abandoned-checkouts` en `GET /catalog/products`
  worden herhaald met oplopende (1-based) `page`; een lege array betekent einde.
- **`GET /orders` levert alles in één keer** — geen paginering aan de adapter-kant.
- **Id-ruimtes uit elkaar houden.** `refundable-items[].id` (LineItem) ≠
  `returnable-items[].id` (FulfillmentLineItem). Gebruik het juiste id in de bijbehorende
  create-call.
- **Schrijf-resultaten hebben altijd `success`.** Callers lezen `result["success"]`; lever
  dat veld altijd (of laat de 404-conversie het invullen).
- **Alias-velden invullen.** Waar `variant_id` én `variantId` gedocumenteerd staan
  (ProductSearchItem), vul beide met dezelfde waarde.
- **`GET /customers/by-phone` is strikt:** alleen 200 bij precies één match, anders 404.
- **Eerlijk antwoorden, niet autoriseren.** Je endpoints hoeven geen sessie-/eigenaarschaps-
  controle te doen; Ragently verifieert de klant vóór het bellen (zie §5).

---

## 12. Webhooks

**Voor dit contract implementeer je GEEN inkomende webhooks.** Ragently haalt data live op
door jouw endpoints server-to-server aan te roepen (pull/poll); er is geen event dat jouw
backend naar Ragently moet pushen om dit contract te vervullen.

Wil je dat Ragently juist gebeurtenissen naar een *ander* systeem duwt (bijvoorbeeld
uitgaande notificaties), dan valt dat onder de **aparte uitgaande-webhooks-feature** van
Ragently en staat het los van dit Custom Commerce-contract. Voor de integratie hier is het
niet nodig.

---

## 13. OpenAPI

De machine-leesbare, autoritatieve bron van dit contract staat naast dit document:

```
/custom-commerce-openapi.yaml
```

(dezelfde directory). Dat is de bron van waarheid die dit document 1-op-1 spiegelt: elke
endpoint (methode, pad, params, request-body, response-schema), het manifest, de vier
auth-schemes, de capabilities, de dependency-regels en de 404-conventie. Bij enig verschil
is de YAML leidend. Gebruik hem om client-/servermodellen te genereren en om je
implementatie te valideren.
