# Uploads

Een `file`-veld beantwoord je met uploadtokens. Upload eerst elk bestand dat de bezoeker kiest en stuur de tokens daarna mee in het object `files` van de inzending (zie [Inzendingen](./submissions)). Net als inzendingen hebben uploads een `forms:write`-sleutel nodig, dus verstuur ze vanaf je server, nooit vanuit de browser (zie [Houd forms:write-sleutels op je server](./index#houd-formswrite-sleutels-op-je-server)).

## POST /v1/api/forms/{key}/uploads

Slaat één bestand op voor één `file`-veld van de gepubliceerde versie van het formulier en geeft er een token voor terug. Vereist de scope `forms:write`. Het formulier moet actief en gepubliceerd zijn.

Het bestand wordt privé opgeslagen: het heeft geen publieke URL, en de merchant opent het vanuit de inbox in het dashboard zodra een inzending het claimt.

### Padparameters

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `key` | string | Ja | De key van het formulier, bijvoorbeeld `quote-request`. |

### Headers

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `Authorization` | string | Ja | `Bearer {forms_write_key}`. |
| `Content-Type` | string | Ja | `multipart/form-data` (je HTTP-client zet dit als je form data verstuurt). |
| `X-Borkol-Visitor-Ip` | string | Nee | Het IP-adres van de bezoeker (IPv4 of IPv6). De uploadlimiet telt per bezoekersadres. Ontbreekt hij of is het geen geldig IP-adres, dan wordt het adres van de verbinding (je server) gebruikt. Zie [Bezoekers-IP en de limiet per uur](./index#bezoekers-ip-en-de-limiet-per-uur). |

### Bodyparameters

Verstuur de body als `multipart/form-data`.

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `field` | string | Ja | De key van een `file`-veld in de gepubliceerde versie van het formulier, maximaal 64 tekens. |
| `file` | file | Ja | Eén bestand. Het type wordt bepaald aan de hand van de inhoud, niet van de naam of het `Content-Type` dat de browser stuurde, en moet passen bij `accept` van het veld: `image` laat JPEG, PNG, WebP en HEIC/HEIF door; `pdf` laat PDF door. SVG en elk ander type worden geweigerd. Maximaal `max_mb` van het veld (1 tot 20 MB). |

```bash title="Request"
curl -X POST "https://api.borkol.com/v1/api/forms/quote-request/uploads" \
  -H "Authorization: Bearer {forms_write_key}" \
  -H "Accept: application/json" \
  -H "X-Borkol-Visitor-Ip: 203.0.113.24" \
  -F "field=brief" \
  -F "file=@brief.pdf"
```

```javascript title="Request (JS)"
// A Next.js route handler: the browser posts multipart (field, file) here, never to Borkol.
export async function POST(request) {
  // Only trust an IP header your own proxy or host sets.
  const visitorIp = request.headers.get("x-real-ip") ?? "";
  const res = await fetch("https://api.borkol.com/v1/api/forms/quote-request/uploads", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BORKOL_FORMS_KEY}`,
      Accept: "application/json",
      "X-Borkol-Visitor-Ip": visitorIp,
    },
    body: await request.formData(),
  });
  return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
}
```

Status: `201 Created`. Het resultaat is verpakt in `data`.

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `token` | string (UUID) | altijd | Stuur het mee in `files` van de inzending, onder dezelfde veld-key. |
| `name` | string | altijd | De opgeslagen bestandsnaam: de bestandsnaam van de bezoeker zonder pad of stuurtekens, maximaal 150 tekens vóór de extensie, met de extensie die de inhoud bewijst (`jpg`, `png`, `webp`, `heic` of `pdf`). `file` als er van de naam niets overblijft. |
| `size` | integer | altijd | De bestandsgrootte in bytes. |

```json title="Response"
{
  "data": {
    "token": "9b2f0c3e-5a1d-4f7e-8c2b-1d3e4f5a6b7c",
    "name": "brief.pdf",
    "size": 482113
  }
}
```

### Tokens

- Een token hoort bij het formulier en het veld waarvoor het is geüpload. Stuur je het mee voor een ander formulier of een ander veld, dan antwoordt de inzending `422` op `files.{key}`.
- Een token werkt voor **één** inzending. Een inzending die wordt geweigerd (elke `422`) claimt niets, dus dezelfde tokens kun je opnieuw meesturen met de verbeterde inzending.
- Een token verloopt **24 uur** na de upload. Een verlopen token geeft `422` op `files.{key}`; upload het bestand dan opnieuw. Bestanden die geen inzending heeft geclaimd, worden verwijderd zodra hun token is verlopen.
- Upload één bestand per request. Voor een veld met `max_files` boven 1 upload je elk bestand apart en stuur je alle tokens mee in één lijst, maximaal `max_files`.
- De upload kijkt niet naar de regels van het formulier: of het veld wordt getoond, wordt bepaald wanneer de inzending binnenkomt. Tokens voor een veld dat op dat moment verborgen is, worden genegeerd en verlopen ongebruikt.

### Uploadlimiet

Elk formulier accepteert **30 uploads per bezoeker per uur**, geteld per bezoekersadres uit `X-Borkol-Visitor-Ip`, los van de limiet voor inzendingen. Elke poging telt, ook uploads die met `422` stranden. Boven de limiet:

```json title="Response"
{
  "message": "Too many uploads. Try again later."
}
```

De response heeft status `429` en geen `Retry-After`-header. De winkelbrede limiet van 120 requests per minuut geldt ook (zie [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits)).

### Fouten

**422 validatie.** De standaard envelope (zie [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits)):

| Key | Melding | Wanneer |
| --- | --- | --- |
| `field` | De standaardmeldingen van Laravel | Ontbreekt, is geen string of is langer dan 64 tekens. |
| `field` | `Not a file field of this form.` | Geen `file`-veld met die key in de gepubliceerde versie. |
| `file` | De standaardmeldingen van Laravel | Ontbreekt, is geen geüpload bestand of is groter dan 20 MB. |
| `file` | `The file may not be larger than {max} MB.` | Groter dan `max_mb` van het veld. |
| `file` | `This file type is not accepted here.` | De inhoud is geen type dat `accept` van het veld toestaat. |

```json title="Response"
{
  "message": "This file type is not accepted here.",
  "errors": {
    "file": ["This file type is not accepted here."]
  }
}
```

**Andere fouten.**

- `404` als geen formulier van de winkel deze key heeft, of als het een concept is, gearchiveerd, nooit gepubliceerd of verwijderd.
- `403` `{"error":"Module not enabled: forms"}` als de module forms uit staat voor de winkel, en `403` `{"message":"Invalid ability provided."}` als de sleutel `forms:write` mist.
- `401` zoals op elke `/v1/api`-route.
