# Objecten

De Objecten-module is het generieke contentmodel van het platform. Een merchant definieert **objecttypes** (contenttypes zoals "FAQ", "Pagina" of "Winkellocatie") in het client-dashboard, elk met een geordende lijst van getypeerde **properties**. Contentredacteuren maken vervolgens **records** (items) van die types. Je storefront leest beide via de publieke API: de typedefinities vertellen je welke vorm je kunt verwachten, de records bevatten de content.

Dat maakt de module een natuurlijke match voor LLM-gedreven en headless storefronts: haal `/v1/api/objects/types` één keer op om het schema te leren kennen, en render records daarna generiek.

Alle vier de endpoints leven onder `/v1/api/objects`, zijn read-only, vereisen de scope `objects:read` op je API-key, en vereisen dat de `objects`-module is ingeschakeld (anders `403 {"error":"Module not enabled: objects"}`).

## Objecttypes en properties

Een objecttype heeft een `key` (een unieke slug die in URL's wordt gebruikt; de waarden `types`, `records`, `properties` en `bulk` zijn gereserveerd en komen nooit voor), een enkelvoudige `name`, een `plural_name` en `settings` die zeggen of de records worden gepubliceerd en hoe ze een adres krijgen. Elk type heeft automatisch een verplichte `name`-textproperty; de merchant voegt de rest toe.

Het `settings`-object van het type:

| Veld | Type | Beschrijving |
| --- | --- | --- |
| `publishable` | boolean | `true` wanneer records van dit type als concept beginnen en gepubliceerd moeten worden (zie [Publiceren en zichtbaarheid](#publiceren-en-zichtbaarheid)). `false` voor types zonder conceptstap. |
| `slug` | object | Alleen aanwezig wanneer het type zijn records adressen geeft: `{source, prefixes}`. `source` is de key van de tekstproperty waarvan het adres is afgeleid. `prefixes` koppelt een taaltag aan het adresvoorvoegsel voor die taal, bijvoorbeeld `{"en": "/services", "nl": "/diensten"}`; een lege string betekent geen voorvoegsel. Afwezig wanneer het type geen adressen heeft. |

Een propertydefinitie heeft:

| Veld | Type | Beschrijving |
| --- | --- | --- |
| `key` | string | Machinekey, passend op `/^[a-z][a-z0-9_]*$/`. Dit is de key die binnen record-`values` wordt gebruikt. |
| `label` | string | Leesbaar label. |
| `type` | string | Eén van de onderstaande propertytypes. |
| `options` | array of string or null | De toegestane waarden voor `select`- en `token_variant`-properties; `null` voor alle andere types. |
| `localized` | boolean | `true` wanneer de waarde één keer per contenttaal wordt opgeslagen (zie [Gelokaliseerde waarden](#gelokaliseerde-waarden)). |
| `config` | object or null | Configuratie per soort: `{fields, item_label_field}` voor een `repeater`, `{object_type_keys}` voor `record_ref` en `record_ref_list`. `null` voor de overige types. |
| `is_required` | boolean | Of de property een waarde moet hebben. Voor een gelokaliseerde property betekent dit een waarde in de standaardtaal. |
| `position` | integer | Sorteerpositie binnen het type, beginnend bij 0. |

Propertytypes en de waarde die elk oplevert binnen `values`. Dezelfde soorten zijn beschikbaar voor velden binnen secties en repeaterrijen.

| Type | Waarde in `values` | Beperkingen |
| --- | --- | --- |
| `text` | string | Maximaal 255 tekens. |
| `textarea` | string | Maximaal 10000 tekens. |
| `richtext` | string (HTML) | Maximaal 50000 tekens. Server-side gesaneerd tot `p`, `br`, `strong`, `em`, `ul`, `ol`, `li`, `a[href]`, `img[src, alt]`, `h2`, `h3`, `h4`. Alleen `https://`, `http://`, root-relatieve `/`, `mailto:` en `tel:` hrefs overleven de sanering. Een `img` behoudt alleen `src` en `alt` en wordt verwijderd tenzij `src` `https://` of root-relatief `/` is. |
| `number` | number | Integer of float. |
| `money` | integer | Niet-negatief, in centen. `15900` betekent 159,00 in de winkelvaluta. |
| `boolean` | boolean | |
| `select` | string | Eén van de `options` van de property. |
| `date` | string | `YYYY-MM-DD`. |
| `datetime` | string | ISO 8601 met offset, bijvoorbeeld `2026-10-12T10:00:00+02:00`. |
| `customer` | string (uuid) | Een klant-id. |
| `media` | object or null | `{id, url, alt, width, height, mime_type}` van een publiek item uit de mediabibliotheek; zie [Mediawaarden](#mediawaarden). |
| `media_list` | array of object | Maximaal 50 mediaobjecten, elk met een extra `caption`; zie [Mediawaarden](#mediawaarden). |
| `link` | object `{url, label}` | `url` verplicht, maximaal 2000 tekens, zelfde toegestane schema's als richtext-hrefs; `label` optioneel, maximaal 200 tekens. |
| `icon` | string | Icoonnaam passend op `/^[a-z][a-z0-9-]{0,39}$/`. |
| `token_variant` | string | Eén van de `options` van de property. |
| `color` | string | `#RRGGBB`, bijvoorbeeld `#1A2B3C`. |
| `email` | string | Een geldig e-mailadres, maximaal 255 tekens. |
| `phone` | string | 5 tot 30 tekens: cijfers, spaties, `()-.`, met een optionele `+` vooraan. |
| `tags` | array of string | Maximaal 50 tags van 1 tot 60 tekens, dubbelen verwijderd. |
| `record_ref` | string (uuid) | Het id van een ander record, van een van de types in `config.object_type_keys` van de property. Haal het op met `GET /v1/api/objects/records/{id}`. |
| `record_ref_list` | array of string (uuid) | Verwijzingen naar andere records; maximaal 24 entries. |
| `repeater` | array of row objects | Elke rij is `{"_key": "row_{24 hex}", ...velden}`, met de velden uit `config.fields` van de property. Maximaal 50 rijen. Een rij kan nog één repeater bevatten waarvan de rijen geen repeater bevatten, en een repeater heeft maximaal 40 velden. Een repeater zelf kan niet gelokaliseerd worden, zijn velden wel. |
| `sections` | array | Gestructureerde page-builder-content; zie hieronder. |

Properties kunnen in het dashboard gearchiveerd worden. Gearchiveerde properties zijn onzichtbaar op de publieke API: ze worden weggelaten uit de propertylijst van het type en hun keys worden uit record-`values` verwijderd.

## Records

Een record is `{id, values, published_at, created_at}`, plus `slugs` wanneer het type adressen heeft. `values` is een JSON-object met propertykeys als sleutel. Properties zonder waarde ontbreken simpelweg in `values` (null-waarden worden bij het opslaan weggelaten), dus behandel ontbrekende keys altijd als "geen waarde". De enige uitzondering is een `media`-waarde die naar een ontbrekend of privé-item wijst: die key is aanwezig met de waarde `null`. `published_at` en `created_at` zijn ISO 8601-timestamps; `published_at` is het moment waarop het record live ging (voor een type zonder publicatiestap is het de aanmaaktijd).

`slugs` koppelt een taaltag aan het adres van het record in die taal, bijvoorbeeld `{"en": "kitchen-fitting", "nl": "keukenmontage"}`. De slug is het laatste padsegment: het volledige pad op je storefront is het voorvoegsel van het type voor die taal (uit `settings.slug.prefixes`) plus `/` plus de slug. Alleen ingeschakelde contenttalen verschijnen. Zoek een record op adres op met `GET /v1/api/objects/types/{key}/records/by-slug/{slug}` (zie [Records](./records)).

## Publiceren en zichtbaarheid

De publieke API levert alleen content die een merchant heeft gepubliceerd.

- Op een type met `settings.publishable: true` is een record zichtbaar wanneer het gepubliceerd is en zijn `published_at` nu of in het verleden ligt. Concepten en records die zijn ingepland voor een toekomstige `published_at` ontbreken in de lijst en geven `404` op beide endpoints voor één record. Een ingepland record verschijnt vanzelf wanneer zijn tijd komt; op dat moment gaat er geen webhook af.
- Op een type zonder publicatiestap hebben records geen conceptstap en zijn ze zichtbaar zodra ze zijn aangemaakt. (Concepten die zijn blijven staan nadat Publiceren is uitgezet, blijven verborgen.)
- De zichtbaarheid is op elk endpoint gelijk. `filter[status]` bestaat niet op de publieke API (het geeft `422`), omdat elk record dat je kunt lezen al gepubliceerd is.

## Gelokaliseerde waarden

Een property of sectieveld met de vlag `localized` slaat één waarde per contenttaal op. De waarde in `values` is een map van taaltag naar waarde, `{"en": "Hello", "nl": "Hallo"}`, met tags passend op `/^[a-z]{2}(-[A-Z]{2})?$/`. De API snijdt de map terug tot de talen die op dat moment voor de winkel zijn ingeschakeld, zodat een taal die de merchant inmiddels heeft uitgezet nooit lekt. Een taal die de merchant leeg liet, ontbreekt gewoon in de map: val terug op de standaardtaal van de winkel, die altijd de eerste ingeschakelde taal is en altijd is ingevuld wanneer de property verplicht is. Een waarde die niet verplicht is mag geen waarde voor de standaardtaal hebben, bijvoorbeeld `{"nl": "Hallo"}` bij een winkel met standaard `en`; blijft er geen ingeschakelde taal over in de map, dan is de waarde `null`.

Een property die niet `localized` is, geeft altijd een kale waarde, welke soort ook. Number, money, boolean, date, datetime, customer, color, media_list, recordverwijzingen, repeater en sections kunnen niet gelokaliseerd worden (de velden binnen een repeaterrij of een sectie wel). Ondersteun beide vormen voor dezelfde soort, want een merchant kan een property later alsnog lokaliseren.

## Mediawaarden

`media`- en `media_list`-waarden worden voor je opgelost, zodat een storefront geen tweede request nodig heeft:

```json title="media value"
{
  "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
  "url": "https://cdn.borkol.com/t/abc/hero.webp",
  "alt": "Oak table in a sunny room",
  "width": 1600,
  "height": 900,
  "mime_type": "image/webp"
}
```

| Veld | Type | Beschrijving |
| --- | --- | --- |
| `id` | string (uuid) | Het mediabibliotheek-id. |
| `url` | string | Publieke URL van het bestand. |
| `alt` | string of null | Alternatieve tekst uit de mediabibliotheek. |
| `width`, `height` | integer of null | Afmeting in pixels; `null` wanneer die niet bekend is (bijvoorbeeld bij bestanden die geen afbeelding zijn). |
| `mime_type` | string | Bijvoorbeeld `image/webp`. |

Een `media`-waarde is dit object, of `null` wanneer het item niet meer bestaat of privé is. Een `media_list`-waarde is een lijst van deze objecten, elk met een extra `caption` (`string`, een locale-map of `null`); een item kan een eigen `alt` hebben die die van de bibliotheek vervangt (een string of een locale-map). Items die ontbreken of privé zijn, vallen uit de lijst, dus de lijst kan korter zijn dan wat de merchant koos. Alleen publieke media wordt ooit getoond; privébestanden verschijnen nooit.

## Sections-waarden

Een property van type `sections` bevat page-builder-content: de waarde is een **lijst van sectie-instanties**. Een opgeslagen lege sections-waarde geeft `[]`. Elke instantie heeft deze vorm:

```json title="Section instance"
{
  "_key": "sec_a1b2c3d4e5f60718293a4b5c",
  "type": "hero",
  "look": "default",
  "v": 1,
  "visible": true,
  "data": { "title": { "en": "Welcome" }, "cta": { "url": "/products", "label": "Shop now" } }
}
```

| Veld | Type | Beschrijving |
| --- | --- | --- |
| `_key` | string | Stabiele instantiekey, `sec_` plus 24 hexadecimale tekens. |
| `type` | string | De sectietype-key, per winkel gedefinieerd in het client-dashboard. |
| `look` | string | De key van de visuele variant (look) van dat sectietype. |
| `v` | integer | Schemaversie van het sectietype op het moment van opslaan. |
| `visible` | boolean | Redacteuren kunnen een sectie verbergen zonder die te verwijderen. Sla instanties met `visible: false` over bij het renderen. |
| `data` | object | Veldwaarden, met de veldkeys van het sectietype als sleutel. |

Sectie-instanties worden bij het schrijven gevalideerd tegen de sectietypedefinities van de winkel (key, naam, icoon, looks, schemaversie, velden, itemlabelveld), die in het client-dashboard worden beheerd. De publieke API stelt de sectietypedefinities niet beschikbaar; je storefront en de merchant spreken het sectievocabulaire buiten de API om af.

Elke veldsoort uit de propertytabel hierboven kan binnen `data` voorkomen, met dezelfde waardevorm en dezelfde publieke serialisatie: `media`- en `media_list`-waarden zijn mediaobjecten, gelokaliseerde velden zijn locale-maps die zijn teruggesneden tot de ingeschakelde talen, en repeaterrijen behouden hun `_key`. Een sectie waarvan het type niet meer in de winkel bestaat, valt uit de lijst. Secties met `visible: false` worden wel teruggegeven; sla ze over bij het renderen.

**Limieten.** Een sectie heeft maximaal 40 velden. Een sections-property bevat maximaal 100 secties (of het geconfigureerde maximum van de property) en maximaal 256KB aan JSON.

## Conventies

- Alle id's zijn UUID's. Alle data is impliciet beperkt tot jouw winkel via de API-key.
- Geld is altijd integer centen. Datums in `date`-velden en bereikfilters zijn `YYYY-MM-DD`-strings. `created_at`- en `published_at`-timestamps zijn ISO 8601.
- Alleen het recordlijst-endpoint is gepagineerd; de typeslijst retourneert een kale `data`-array.

## Endpoints

| Endpoint | Doel |
| --- | --- |
| `GET /v1/api/objects/types` | Alle objecttypes met hun propertydefinities. Zie [Types](./types). |
| `GET /v1/api/objects/types/{key}/records` | Gepagineerde, filterbare recordlijst voor één type. Zie [Records](./records). |
| `GET /v1/api/objects/types/{key}/records/by-slug/{slug}` | Eén record ophalen op zijn adres in één taal. Zie [Records](./records). |
| `GET /v1/api/objects/records/{id}` | Eén record ophalen op id. Zie [Records](./records). |
