# Bestellingen

Een bestelling wordt aangemaakt door de checkout (of door het accepteren van een offerte) in status `pending_payment`. Vanuit de storefront API zijn bestellingen read-only: alle statuswijzigingen (betaling, verzending, terugbetalingen) gebeuren aan merchant-zijde in het dashboard. Beide endpoints vereisen de `commerce:read` scope en de commerce-module.

## Bestelstatus

| Status | Betekenis | Kan overgaan naar |
| --- | --- | --- |
| `pending_payment` | Aangemaakt, wacht op betaling. | `paid`, `cancelled` |
| `paid` | Betaling ontvangen. | `shipped`, `cancelled`, `refunded` |
| `shipped` | Overgedragen aan de vervoerder; trackingvelden kunnen gezet zijn. | `completed`, `refunded` |
| `completed` | Afgehandeld. | `refunded` |
| `cancelled` | Geannuleerd vóór afhandeling. De voorraad wordt teruggeboekt. | terminaal |
| `refunded` | Terugbetaald. De voorraad wordt teruggeboekt. | terminaal |

Behandel de enum als open: een client moet een onbekende toekomstige status neutraal weergeven in plaats van te falen.

## De vorm van de bestelrespons

Beide endpoints serialiseren bestellingen identiek (enkele bestellingen verpakt in `{"data": {...}}`, geschiedenisitems als elementen van een gepagineerde `data`-array). Alle geldbedragen zijn objecten `{"amount": int, "currency": "EUR"}` met `amount` in minor units. Datums zijn ISO-8601-strings.

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | altijd | Bestelling-id. |
| `number` | integer | altijd | Klantgericht bestelnummer uit een teller per winkel. Gewone integer, geen UUID. |
| `status` | string | altijd | Zie de statustabel hierboven. |
| `email` | string | altijd | Het e-mailadres opgegeven bij de checkout. |
| `billing_address` | object | altijd | Het factuuradres zoals ingediend (`name`, `street`, `postal_code`, `city`, `country`). |
| `shipping_address` | object of null | altijd | Het verzendadres, of null wanneer verzonden wordt naar het factuuradres. |
| `currency` | string | altijd | `EUR`. |
| `lines` | array | altijd | Bestelregels, zie hieronder. |
| `subtotal` | money | altijd | Goederentotaal exclusief btw. |
| `tax_total` | money | altijd | Totale btw inclusief btw op verzending. |
| `total` | money | altijd | Eindtotaal inclusief verzending. |
| `tax_breakdown` | array | altijd | Uitsplitsing per tarief, elk `{"rate": float, "base": int, "tax": int}`. |
| `discount_total` | money | altijd | Totale goederenkorting toegepast bij checkout, exclusief een eventuele gratis-verzendkorting. `amount` is `0` wanneer geen korting is toegepast. |
| `discount_breakdown` | array of null | altijd | Eén item per toegepaste korting, elk `{"discount_id": uuid, "name": string, "code": string of null, "kind": "order" / "products" / "bogo" / "shipping", "amount": int}`. Null wanneer geen korting is toegepast. Bij een `shipping`-item is `amount` de verzendkosten die zijn kwijtgescholden. |
| `shipping_method_name` | string of null | altijd | Naam-snapshot van de gekozen verzendmethode, null wanneer geen verzending is toegepast. |
| `shipping` | money | altijd | Verzendkosten exclusief btw. `amount` is null wanneer geen verzending is toegepast. |
| `shipping_tax` | money | altijd | Btw op verzending. `amount` is null wanneer geen verzending is toegepast. |
| `tracking_carrier` | string of null | altijd | Gezet door de merchant bij verzending. |
| `tracking_number` | string of null | altijd | Gezet door de merchant bij verzending. |
| `invoice_number` | integer of null | altijd | Factuurteller per winkel, gezet wanneer de merchant de bestelling factureert. |
| `invoiced_at` | string (ISO-8601) of null | altijd | Wanneer de bestelling is gefactureerd. |
| `customer_id` | string (UUID) of null | altijd | Het contact waaraan de bestelling gekoppeld is, null voor gastbestellingen. |
| `contact_id` | string (UUID) of null | altijd | Dezelfde waarde als `customer_id`; de canonieke naam vanaf nu. |
| `placed_at` | string (ISO-8601) of null | altijd | Wanneer de bestelling is geplaatst. |
| `created_at` | string (ISO-8601) of null | altijd | Aanmaaktijd van de rij. |
| `invoice_id` | string (UUID) of null | altijd | De sales-factuur die uit deze order is aangemaakt, indien aanwezig. Null wanneer er geen factuur is of de winkel de sales-module niet gebruikt. |

### `lines[]`

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | altijd | Bestelregel-id. |
| `product_id` | string (UUID) | altijd | Het product. |
| `variant_id` | string (UUID) of null | altijd | De variant, indien van toepassing. |
| `variant_label` | string of null | altijd | Leesbaar variantlabel-snapshot, bijvoorbeeld `"Green / M"`. |
| `name` | string | altijd | Productnaam-snapshot. |
| `sku` | string of null | altijd | SKU-snapshot. |
| `quantity` | number | altijd | Bestelde hoeveelheid: een geheel getal voor een `piece`-product, tot drie decimalen voor elke andere eenheid (bijvoorbeeld `0.75`). |
| `unit` | string | altijd | De eenheid van het product op het moment van de order: `piece`, `kg`, `g`, `l`, `m`, `m2`, `m3` of `hour`. |
| `unit_price` | money | altijd | Stukprijs. |
| `line_subtotal` | money | altijd | Regeltotaal exclusief btw. |
| `tax_rate` | number | altijd | Btw-percentage toegepast op de regel. |
| `tax` | money | altijd | Btw op de regel. |
| `line_total` | money | altijd | Regeltotaal inclusief btw. |
| `attributes` | object of null | altijd | Regelsnapshot vastgelegd bij het plaatsen van de order, zie hieronder. |

### `lines[].attributes`

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `returnable` | boolean | conditioneel | Of de regel geretourneerd kan worden. Altijd `false` voor een `made_to_measure`-product; anders de `returnable` van het product op het moment van de order. Een regel zonder product (een eigen offerteregel, of een product dat is verwijderd voordat de offerte werd geaccepteerd) is `true`, met `lead_time_days` `null`. |
| `lead_time_days` | integer of null | conditioneel | De levertijd van het product in dagen op het moment van de order, `null` wanneer die er niet was. |
| `configuration` | object | conditioneel | Alleen made-to-measure-regels: de configuratiesnapshot (dimensions, choices, breakdown, components, matrix), zoals op de cartregel. `components[].qty` is een exacte decimale string. |

`returnable` en `lead_time_days` worden vanaf 2026-10-11 vastgelegd op elke regel van orders die via de checkout, een handmatige order van de merchant of een geaccepteerde offerte zijn geplaatst. Eerdere orders kunnen `attributes` null hebben of zonder deze keys: behandel een ontbrekende waarde als onbekend.

## GET /v1/api/commerce/orders/{id}

Haalt een enkele bestelling met regels op via id. Er is geen controle op klant-eigenaarschap op dit endpoint: het wordt puur geautoriseerd door de tenant-API-key, wat een server-side secret is. Het bestaat voor de storefront-server, bijvoorbeeld voor het renderen van de orderbevestigingspagina direct na de checkout. Proxy het nooit naar browsers met een door de client aangeleverd bestelling-id, tenzij je server eerst zelf een eigenaarschapscontrole uitvoert.

### Parameters

**Pad**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | ja | Bestelling-id. |

```bash title="Request"
curl https://api.borkol.com/v1/api/commerce/orders/7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1 \
  -H "Authorization: Bearer {api_key}"
```

```json title="Response"
{
  "data": {
    "id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
    "number": 1042,
    "status": "paid",
    "email": "jane@example.com",
    "billing_address": {
      "name": "Jane Doe",
      "street": "Keizersgracht 1",
      "postal_code": "1015 CC",
      "city": "Amsterdam",
      "country": "NL"
    },
    "shipping_address": null,
    "currency": "EUR",
    "lines": [
      {
        "id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "variant_label": "Green / M",
        "name": "Garden Chair",
        "sku": "CHAIR-GRN-M",
        "quantity": 2,
        "unit": "piece",
        "unit_price": { "amount": 2250, "currency": "EUR" },
        "line_subtotal": { "amount": 3347, "currency": "EUR" },
        "tax_rate": 21,
        "tax": { "amount": 703, "currency": "EUR" },
        "line_total": { "amount": 4050, "currency": "EUR" },
        "attributes": { "returnable": true, "lead_time_days": null }
      }
    ],
    "subtotal": { "amount": 3347, "currency": "EUR" },
    "tax_total": { "amount": 789, "currency": "EUR" },
    "total": { "amount": 4545, "currency": "EUR" },
    "tax_breakdown": [{ "rate": 21, "base": 3756, "tax": 789 }],
    "discount_total": { "amount": 0, "currency": "EUR" },
    "discount_breakdown": null,
    "shipping_method_name": "Standard",
    "shipping": { "amount": 409, "currency": "EUR" },
    "shipping_tax": { "amount": 86, "currency": "EUR" },
    "tracking_carrier": "PostNL",
    "tracking_number": "3STEST123456",
    "invoice_number": null,
    "invoiced_at": null,
    "customer_id": null,
    "contact_id": null,
    "placed_at": "2026-08-14T10:15:30+00:00",
    "created_at": "2026-08-14T10:15:30+00:00",
    "invoice_id": null
  }
}
```

### Fouten

| Status | Body | Wanneer |
| --- | --- | --- |
| 404 | `{"message": "..."}` | Onbekend of soft-deleted bestelling-id, of een bestelling van een andere winkel. |
| 403 | scope-, module- of suspensie-body | Key mist `commerce:read`, module uitgeschakeld, of winkel geschorst. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit overschreden. |

## GET /v1/api/commerce/my/orders

Gepagineerde bestelgeschiedenis voor de klant geïdentificeerd door `X-Customer-Token`, gesorteerd op `placed_at` aflopend (nieuwste eerst). Het token is verplicht: dit is het endpoint achter de "mijn bestellingen"-pagina van een storefront. Gebruikt de standaard Laravel-paginatie-envelope.

### Parameters

**Query**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `per_page` | integer | nee | Paginagrootte, standaard 25, begrensd tot 1..100. |
| `page` | integer | nee | Paginanummer, standaard 1. |

**Headers**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | ja | Klantsessietoken. Ontbrekend of niet herleidbaar: 401. |

```bash title="Request"
curl "https://api.borkol.com/v1/api/commerce/my/orders?per_page=25&page=1" \
  -H "Authorization: Bearer {api_key}" \
  -H "X-Customer-Token: {customer_token}"
```

```javascript title="Request (JS)"
const res = await fetch(
  "https://api.borkol.com/v1/api/commerce/my/orders?per_page=25",
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "X-Customer-Token": customerToken,
    },
  }
);
const { data: orders, meta } = await res.json();
```

```json title="Response"
{
  "data": [
    {
      "id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
      "number": 1042,
      "status": "shipped",
      "email": "jane@example.com",
      "billing_address": {
        "name": "Jane Doe",
        "street": "Keizersgracht 1",
        "postal_code": "1015 CC",
        "city": "Amsterdam",
        "country": "NL"
      },
      "shipping_address": null,
      "currency": "EUR",
      "lines": [
        {
          "id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
          "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "variant_id": null,
          "variant_label": null,
          "name": "Garden Chair",
          "sku": "CHAIR-GRN",
          "quantity": 2,
          "unit": "piece",
          "unit_price": { "amount": 2250, "currency": "EUR" },
          "line_subtotal": { "amount": 3719, "currency": "EUR" },
          "tax_rate": 21,
          "tax": { "amount": 781, "currency": "EUR" },
          "line_total": { "amount": 4500, "currency": "EUR" },
          "attributes": { "returnable": true, "lead_time_days": 3 }
        }
      ],
      "subtotal": { "amount": 3719, "currency": "EUR" },
      "tax_total": { "amount": 867, "currency": "EUR" },
      "total": { "amount": 4995, "currency": "EUR" },
      "tax_breakdown": [{ "rate": 21, "base": 4128, "tax": 867 }],
      "discount_total": { "amount": 0, "currency": "EUR" },
      "discount_breakdown": null,
      "shipping_method_name": "Standard",
      "shipping": { "amount": 409, "currency": "EUR" },
      "shipping_tax": { "amount": 86, "currency": "EUR" },
      "tracking_carrier": "PostNL",
      "tracking_number": "3STEST123456",
      "invoice_number": null,
      "invoiced_at": null,
      "customer_id": "0a1b2c3d-4e5f-4a6b-7c8d-9e0f1a2b3c4d",
      "contact_id": "0a1b2c3d-4e5f-4a6b-7c8d-9e0f1a2b3c4d",
      "placed_at": "2026-08-10T14:02:11+00:00",
      "created_at": "2026-08-10T14:02:11+00:00",
      "invoice_id": null
    }
  ],
  "links": {
    "first": "https://api.borkol.com/v1/api/commerce/my/orders?page=1",
    "last": "https://api.borkol.com/v1/api/commerce/my/orders?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      { "url": null, "label": "&laquo; Previous", "active": false },
      { "url": "https://api.borkol.com/v1/api/commerce/my/orders?page=1", "label": "1", "active": true },
      { "url": null, "label": "Next &raquo;", "active": false }
    ],
    "path": "https://api.borkol.com/v1/api/commerce/my/orders",
    "per_page": 25,
    "to": 1,
    "total": 1
  }
}
```

### Fouten

| Status | Body | Wanneer |
| --- | --- | --- |
| 401 | `{"message": "Invalid customer token."}` | `X-Customer-Token` ontbreekt of is niet herleidbaar. |
| 403 | scope-, module- of suspensie-body | Key mist `commerce:read`, module uitgeschakeld, of winkel geschorst. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit overschreden. |
