# REST API

Eén API voor elke resource: sessie-, token- en publieke toegang, lezen, schrijven, export, import en bestanden.

Elke resource heeft dezelfde REST API. Definieer een resource en hij antwoordt op `/api/v1/<resource>`: tonen, lezen, aanmaken, wijzigen, verwijderen, exporteren en importeren, met de rechten van wie de aanroep doet. Leer het voor één resource en je kent het voor allemaal. De voorbeelden op deze pagina gebruiken `projects`.

| Request | Doet | Recht |
|---|---|---|
| `GET /api/v1/projects` | Records tonen, met filteren, sorteren, zoeken en pagineren | `view-projects` |
| `GET /api/v1/projects/<uuid>` | Eén record lezen | `view-projects` |
| `POST /api/v1/projects` | Een record aanmaken | `create-projects` |
| `POST /api/v1/projects/<uuid>` | Een record aanmaken met een id die je zelf kiest | `create-projects` |
| `PUT` of `PATCH /api/v1/projects/<uuid>` | De velden wijzigen die je stuurt | `edit-projects` |
| `PUT` of `PATCH /api/v1/projects` | Meerdere records in één request wijzigen | `edit-projects` |
| `DELETE /api/v1/projects/<uuid>` | Eén record verwijderen | `delete-projects` |
| `DELETE /api/v1/projects` | Alle records verwijderen | `api_delete_resource_projects` |
| `GET /api/v1/projects/export` | Alle records downloaden als JSON, CSV of TSV | `export-projects` |
| `POST /api/v1/projects/import` | Records aanmaken en wijzigen vanuit een bestand | `import-projects` |

`manage-projects` omvat elke handeling behalve alle records in één keer verwijderen; dat staat alleen het eigen recht daarvoor toe.

## Wie de aanroep doet

Er zijn drie manieren om binnen te komen, en de rechten zijn voor elk hetzelfde.

| Aanroeper | Logt in met | Gebruikelijk voor |
|---|---|---|
| Je eigen pagina's en scripts | De sessie van de ingelogde gebruiker | Formulieren, inline bewerken, een applicatie op een route |
| Een ander systeem | Een token | Een app, een server, een integratie |
| Een anonieme bezoeker | Een uitdrukkelijke toestemming per handeling | Een contact- of aanmeldformulier |

### Vanaf je eigen pagina's: `nb.api`

Code op de site zelf heeft geen token nodig. De browser stuurt de sessie van de ingelogde gebruiker mee, en de API past de rechten van die gebruiker toe, precies zoals in de admin. De pagina-shell levert een kleine client, `nb.api`, en het basisadres van de site in `nb.base_url`:

```js
const list = await nb.api.get(`${nb.base_url}/api/v1/projects?filter=published:1&limit=12`);

const created = await nb.api.post(`${nb.base_url}/api/v1/projects`, {
    title: 'Harbour Atlas',
    sector: 'public',
});

await nb.api.put(`${nb.base_url}/api/v1/projects/${uuid}`, { published: true });
await nb.api.delete(`${nb.base_url}/api/v1/projects/${uuid}`);
```

Elke aanroep geeft het ingelezen JSON-antwoord terug, ook bij een fout, dus controleer `success`:

```js
const answer = await nb.api.post(`${nb.base_url}/api/v1/leads`, form);
if (!answer.success) {
    show_error(answer.message);
}
```

Elke andere client werkt op dezelfde manier, zolang hij de sessiecookie meestuurt: `fetch(url, { credentials: 'same-origin' })` vanuit React, Vue of gewoon JavaScript. Een bezoeker die niet is ingelogd, heeft de rechten van een anonieme bezoeker, en dat zijn er standaard geen.

### Vanuit een ander systeem: tokens

Een externe aanroeper logt in als Nimbly-gebruiker en krijgt een token.

**1. Geef de aanroeper een eigen gebruiker.** Maak een rol met alleen de rechten die nodig zijn, bijvoorbeeld `view-projects` voor een feed, en een gebruiker met die rol. Zet **API access** aan in het actiepaneel van de gebruiker in de admin, naast het bewerkformulier op `/nb-admin/users/<uuid>`; daarvoor is een rol nodig die gebruikers mag bewerken. Zet je het uit, dan werkt het token van die gebruiker meteen niet meer.

**2. Vraag een token aan:**

```bash
curl -X POST https://example.com/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"email": "feed@example.com", "password": "…"}'
```

```json
{
  "token": "anSojGGKOveDuIypVGbx…",
  "token_created": 1791940697,
  "token_expires": 1792075420,
  "code": 200,
  "success": true,
  "status": "ok"
}
```

**3. Stuur het mee met elk request:**

```bash
curl https://example.com/api/v1/projects \
  -H "Authorization: Bearer $TOKEN"
```

Een token is 10 minuten geldig na de laatste vernieuwing. Vernieuw het voor die tijd met een `GET` op hetzelfde adres:

```bash
curl https://example.com/api/v1/auth/token -H "Authorization: Bearer $TOKEN"
```

Een fout wachtwoord geeft `401 INVALID_CREDENTIALS`; een gebruiker zonder API-toegang geeft `403 ACCESS_DENIED`. Bewaar de inloggegevens in de eigen geheimenkluis van de aanroeper, nooit in een repository.

### Vanaf anonieme bezoekers: één handeling tegelijk

Een publiek formulier moet records kunnen aanmaken zonder dat iemand inlogt. Sta precies die handeling toe, en verder niets, met een route op `ext/uri/api/v1/<resource>/index.tpl`:

```html
[#api-allow post leads#]
```

Elke andere handeling op de resource blijft dicht. Formulieren die met `[#build-form#]` zijn gemaakt, voegen een formuliersleutel en een honeypotveld toe, die de API controleert. Zie [Formulieren](/nl/docs/guides/forms/).

## Lezen

### Een lijst

```bash
GET /api/v1/projects?filter=published:1&sort=year|numeric|desc&limit=12&offset=0
```

```json
{
  "projects": {
    "4f1c9a2e7b": {"uuid": "4f1c9a2e7b", "title": "Harbour Atlas", "sector": "public", "year": 2026, "published": true, "_created": 1792074820, "_modified": 1792074830},
    "9d03b6e1c4": {"uuid": "9d03b6e1c4", "title": "Northlight Library", "sector": "culture", "year": 2026, "published": true, "_created": 1792074821, "_modified": 1792074821}
  },
  "count": 2,
  "total": 9,
  "code": 200,
  "success": true,
  "status": "ok"
}
```

Records zijn een object met de UUID als sleutel. `count` is het aantal dat terugkomt; `total`, aanwezig als je filtert, sorteert of pagineert, is het aantal vóór het afknippen.

| Parameter | Voorbeeld | Doet |
|---|---|---|
| `filter` | `status:open`, `status:!draft`, `status:new\|\|todo`, `published:1,sector:public` | Houdt passende records over. `!` keert om, `\|\|` is of, een komma is en |
| `sort` | `year\|numeric\|desc`, `title\|text\|asc`, `date\|desc,title\|asc` | Sorteert op één of meer velden |
| `limit`, `offset` | `limit=20&offset=40` | Eén pagina: de derde pagina van 20 |
| `search` | `search=harbour` | Records waarvan de titel de tekst bevat; in een resource zonder titelveld, elk veld |

Een gefilterde, gesorteerde of gepagineerde lijst leest alleen de records op die pagina, ook bij een grote resource. Filteren of sorteren op een veld dat de API nooit teruggeeft, wordt geweigerd met `400`.

### Eén record

```bash
GET /api/v1/projects/4f1c9a2e7b
```

Het antwoord heeft dezelfde vorm als een lijst, met één record.

### Ongewijzigde data

Leesacties antwoorden met een header `Last-Modified`. Stuur die terug als `If-Modified-Since`, dan antwoordt een ongewijzigd record of een ongewijzigde lijst met `304 Not Modified`, zonder inhoud.

## Schrijven

Stuur JSON met `Content-Type: application/json`. Waarden worden gevalideerd tegen de definitie van de resource, opgemaakte tekst wordt opgeschoond tot de toegestane markup, en elke wijziging komt in de geschiedenis van het record, met de gebruiker die hem deed.

### Aanmaken

```bash
POST /api/v1/projects
{"title": "Harbour Atlas", "sector": "public", "year": 2026}
```

```json
{
  "projects": {"1ztuvo56o5vze6t0": {"uuid": "1ztuvo56o5vze6t0", "title": "Harbour Atlas", "sector": "public", "year": 2026}},
  "count": 1,
  "message": "RESOURCE_CREATED",
  "code": 201,
  "success": true,
  "status": "ok"
}
```

Nimbly maakt de UUID. Wil je een id uit een ander systeem gebruiken, zet die dan in het adres, `POST /api/v1/projects/erp-1042`, of stuur hem mee als `uuid` in de inhoud. Een id die al bestaat, geeft `409 RESOURCE_EXISTS`; een ontbrekend verplicht veld of een ongeldige waarde geeft `422 INVALID_DATA`.

### Wijzigen

```bash
PUT /api/v1/projects/1ztuvo56o5vze6t0
{"published": true}
```

Alleen de velden die je stuurt, veranderen. `PATCH` doet precies hetzelfde; gebruik wat je client verwacht. Het antwoord bevat het hele record zoals het nu is, met `RESOURCE_UPDATED`. Een resource met `"upsert": true` in de `.meta` maakt het record aan als het nog niet bestaat.

Meerdere records in één request, met de UUID als sleutel:

```bash
PUT /api/v1/projects
{
  "1ztuvo56o5vze6t0": {"published": true},
  "9d03b6e1c4": {"published": false}
}
```

### Verwijderen

```bash
DELETE /api/v1/projects/1ztuvo56o5vze6t0
```

geeft `RESOURCE_DELETED`. Het record is nog 90 dagen terug te zetten vanuit de geschiedenis in de admin. `DELETE /api/v1/projects` verwijdert alle records en heeft een eigen recht nodig, `api_delete_resource_projects`, dat in geen enkel ander recht zit.

## Exporteren en importeren

### Exporteren

```bash
GET /api/v1/projects/export?format=csv
```

Downloadt alle records als een bestand met de naam van de resource en de datum. `format` is `json` (de standaard), `csv` of `tsv`. De JSON-export bevat de definitie van de resource als `_meta` naast de records, zodat je de resource ergens anders opnieuw kunt maken. Velden die de API nooit teruggeeft, blijven weg.

### Importeren

```bash
curl -X POST https://example.com/api/v1/projects/import \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@projects.csv"
```

```json
{"stats": {"imported": 2, "errors": 0, "created": 1, "updated": 1, "total": 2}, "code": 200, "success": true, "status": "ok"}
```

Het bestand is JSON (een export, een object met records, of een lijst), CSV of TSV met een kopregel met veldnamen. Een rij wijzigt een bestaand record als de `uuid` past, en anders de `id`, `name` of `title`; andere rijen maken nieuwe records. Een JSON-export die wordt geïmporteerd in een site zonder die resource, maakt de resource aan vanuit de `_meta`.

### Een document importeren

`POST /api/v1/projects/import-document` met een `.docx`-bestand stelt veldwaarden voor een nieuw record voor, door een AI-model uit het document gelezen. Dit is wat de actie **Import document** in de admin gebruikt. Het heeft `create-projects` en een OpenAI-sleutel in `.env` nodig, en maakt zelf niets aan.

## Bestanden

Uploads gaan via `.files` naar de mediabibliotheek:

```bash
curl -X POST https://example.com/api/v1/.files \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@cover.jpg"
```

```json
{
  "files": {"uuid": "415efabde63801837fd88c62078f5db0", "name": "cover.jpg", "type": "image/jpeg", "size": 248113, "width": 2400, "height": 1600, "orientation": "landscape"},
  "message": "RESOURCE_CREATED",
  "code": 201
}
```

De UUID is de checksum van het bestand, dus hetzelfde bestand twee keer uploaden geeft dezelfde UUID. Bewaar die in een veld `image`, `gallery` of `file`, net als elke andere waarde.

| Request | Doet |
|---|---|
| `GET /api/v1/.files` | Alle bestanden tonen met hun metadata |
| `GET /api/v1/.files/<uuid>` | De metadata van één bestand lezen |
| `PUT /api/v1/.files/<uuid>` | Metadata wijzigen, zoals `title` |
| `DELETE /api/v1/.files/<uuid>` | Het bestand, de metadata en de verkleinde kopieën verwijderen |
| `GET /img/<uuid>/<size>` | De afbeelding, verkleind: `800w`, `400h`, `400x300c` (uitsnede), `400x300f` (passend), als WebP |
| `GET /download/<uuid>` | Het bestand zoals geüpload, met naam en type |

Rechten voor bestanden stel je in op de rij Files van het rollenscherm. Een bestand dat groter is dan de uploadlimiet van de server, geeft `413 UPLOAD_TOO_LARGE`.

## Antwoorden en fouten

Elk antwoord is JSON met `code`, `success` en `status` naast de data:

```json
{
  "message": "ACCESS_DENIED",
  "needs": "api_get_projects,api_(any)_projects,api_get_(any),api_(any),view-projects,manage-projects",
  "code": 403,
  "success": false,
  "status": "error"
}
```

| Code | Melding | Betekent |
|---|---|---|
| 200 | `RESOURCE_UPDATED`, `RESOURCE_DELETED` | Gedaan |
| 201 | `RESOURCE_CREATED` | Aangemaakt |
| 304 | | Niet gewijzigd sinds `If-Modified-Since` |
| 400 | `INVALID_DATA` | Het request klopt niet, of filtert op een verborgen veld |
| 401 | `INVALID_CREDENTIALS` | Fout e-mailadres of wachtwoord bij een tokenaanvraag |
| 403 | `ACCESS_DENIED` | Niet toegestaan; `needs` noemt de rechten die het zouden toestaan |
| 404 | `RESOURCE_NOT_FOUND` | Die resource of dat record bestaat niet |
| 405 | `METHOD_NOT_ALLOWED` | De resource ondersteunt deze methode niet |
| 409 | `RESOURCE_EXISTS` | Er bestaat al een record met deze id |
| 413 | `UPLOAD_TOO_LARGE` | Het bestand is groter dan de uploadlimiet |
| 422 | `INVALID_DATA` | Een waarde voldoet niet aan de validatie van de resource |
| 500 | `RESOURCE_CREATE_FAILED`, `RESOURCE_UPDATE_FAILED` | De server kon niet schrijven |

Controleer `success` en `code` in plaats van één bepaalde melding.

## Wat de API nooit teruggeeft

Velden in `hidden` in de `.meta`, en eenzijdig versleutelde velden zoals wachtwoorden, worden bewaard en gebruikt, maar verschijnen nooit in een lijst, een record, een antwoord op een schrijfactie of een export. Bij gebruikers zijn het wachtwoord, de salt, het API-token en de hersteltokens altijd verborgen.

## Eigen endpoints

Heeft een integratie een antwoord nodig dat de resource-API niet geeft, schrijf dan een eigen route. `api_access('<permission>')` accepteert alle drie soorten aanroepers, dus één endpoint bedient je eigen pagina's en andere systemen. Zie [Integraties](/nl/docs/guides/integrations/) en [Wat een route teruggeeft](/nl/docs/guides/responses/).

## OpenAPI

Elke Nimbly-site beschrijft zijn eigen API volgens de standaard OpenAPI 3, op `/api/v1/openapi.json`. Het document noemt de resources van de site met hun velden, typen, verplichte velden en keuzeopties, en de beschikbare handelingen. Het volgt de rechten van de aanroeper, precies zoals de API: een token of sessie ziet alleen de resources en handelingen die hij mag gebruiken, velden die de API nooit teruggeeft, blijven weg, en een anoniem request wordt geweigerd.

```bash
curl https://example.com/api/v1/openapi.json -H "Authorization: Bearer $TOKEN" > openapi.json
```

Laad het in Postman, Insomnia of Swagger UI, of maak er met een OpenAPI-generator een getypeerde client van. Geef een integratie een eigen gebruiker, dan laat het document precies zien wat die integratie kan.

Een [algemene beschrijving](/reference/api/openapi.json) van de handelingen die elke resource deelt, met export, import en bestanden, staat hier.

## Interne endpoints

Een paar adressen onder `/api/v1/` dienen de admin en de bewerktools: `.resources`, `.pages`, `.files-usage`, `.files-unused`, `.system-messages`, `session`, `openai/complete`, `openai/translate`, `git-status`, `git-pull`, `empty-resource`, `agent-chat` en `agent-remote`. Ze controleren rechten zoals elk ander endpoint, maar hun vorm kan meeveranderen met de admin; bouw liever op de resource-API.
