Forms
Uploads
Store a visitor's file for a form's file field from your server and get a single-use token to send with the submission.
View as MarkdownA file field is answered with upload tokens. Upload each file the visitor picks first, then send the tokens in the submission's files object (see Submissions). Like submissions, uploads need a forms:write key, so send them from your server, never from the browser (see Keep forms:write keys on your server).
A file field is answered with upload tokens. Upload each file the visitor picks first, then send the tokens in the submission's files object (see Submissions). Like submissions, uploads need a forms:write key, so send them from your server, never from the browser (see Keep forms:write keys on your server).
POST /v1/api/forms/{key}/uploads
Stores one file for one file field of the form's published version and returns a token for it. Requires the forms:write scope. The form must be active and published.
The file is stored privately: it has no public URL, and the merchant opens it from the dashboard inbox once a submission claims it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The form key, for example quote-request. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Bearer {forms_write_key}. |
Content-Type | string | Yes | multipart/form-data (set by your HTTP client when you send form data). |
X-Borkol-Visitor-Ip | string | No | The visitor's IP address (IPv4 or IPv6). The upload limit counts per visitor address. Missing or not a valid IP address: the address of the connection (your server) is used instead. See Visitor IP and the hourly limit. |
Body parameters
Send the body as multipart/form-data.
| Name | Type | Required | Description |
|---|---|---|---|
field | string | Yes | The key of a file field in the form's published version, at most 64 characters. |
file | file | Yes | One file. Its type is judged by its content, not by its name or the Content-Type the browser sent, and must match the field's accept: image lets through JPEG, PNG, WebP and HEIC/HEIF; pdf lets through PDF. SVG and every other type are refused. At most the field's max_mb (1 to 20 MB). |
Status: 201 Created. The result is wrapped in data.
| Name | Type | Required | Description |
|---|---|---|---|
token | string (UUID) | always | Send it in the submission's files under the same field key. |
name | string | always | The stored file name: the visitor's file name without any path or control characters, at most 150 characters before the extension, with the extension its content proves (jpg, png, webp, heic or pdf). file when nothing is left of the name. |
size | integer | always | The file size in bytes. |
Tokens
- A token belongs to the form and the field it was uploaded for. Sent for another form or another field, the submission answers
422onfiles.{key}. - A token works for one submission. A submission that is rejected (any
422) claims nothing, so the same tokens can be sent again with the corrected submission. - A token expires 24 hours after the upload. An expired token answers
422onfiles.{key}; upload the file again. Files that no submission claimed are deleted once their token has expired. - Upload one file per request. For a field with
max_filesabove 1, upload each file and send all tokens in one list, at mostmax_files. - The upload does not look at the form's rules: whether the field is shown is decided when the submission arrives. Tokens for a field that is hidden at that moment are ignored and expire unused.
Upload limit
Each form accepts 30 uploads per visitor per hour, counted per visitor address from X-Borkol-Visitor-Ip, separately from the submission limit. Every attempt counts, including uploads that fail with 422. Over the limit:
The response has status 429 and carries no Retry-After header. The store-wide limit of 120 requests per minute applies as well (see Errors and rate limits).
Errors
422 validation. The standard envelope (see Errors and rate limits):
| Key | Message | When |
|---|---|---|
field | Laravel's standard messages | Missing, not a string or longer than 64 characters. |
field | Not a file field of this form. | No file field with that key in the published version. |
file | Laravel's standard messages | Missing, not an uploaded file, or larger than 20 MB. |
file | The file may not be larger than {max} MB. | Larger than the field's max_mb. |
file | This file type is not accepted here. | The content is not a type the field's accept allows. |
Other errors.
404when no form of the store has that key, or it is a draft, archived, never published or deleted.403{"error":"Module not enabled: forms"}when the forms module is off for the store, and403{"message":"Invalid ability provided."}when the key lacksforms:write.401as on every/v1/apiroute.
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"// 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" } });
}{
"data": {
"token": "9b2f0c3e-5a1d-4f7e-8c2b-1d3e4f5a6b7c",
"name": "brief.pdf",
"size": 482113
}
}{
"message": "Too many uploads. Try again later."
}{
"message": "This file type is not accepted here.",
"errors": {
"file": ["This file type is not accepted here."]
}
}POST /v1/api/forms/{key}/uploads
Stores one file for one file field of the form's published version and returns a token for it. Requires the forms:write scope. The form must be active and published.
The file is stored privately: it has no public URL, and the merchant opens it from the dashboard inbox once a submission claims it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The form key, for example quote-request. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Bearer {forms_write_key}. |
Content-Type | string | Yes | multipart/form-data (set by your HTTP client when you send form data). |
X-Borkol-Visitor-Ip | string | No | The visitor's IP address (IPv4 or IPv6). The upload limit counts per visitor address. Missing or not a valid IP address: the address of the connection (your server) is used instead. See Visitor IP and the hourly limit. |
Body parameters
Send the body as multipart/form-data.
| Name | Type | Required | Description |
|---|---|---|---|
field | string | Yes | The key of a file field in the form's published version, at most 64 characters. |
file | file | Yes | One file. Its type is judged by its content, not by its name or the Content-Type the browser sent, and must match the field's accept: image lets through JPEG, PNG, WebP and HEIC/HEIF; pdf lets through PDF. SVG and every other type are refused. At most the field's max_mb (1 to 20 MB). |
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"// 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. The result is wrapped in data.
| Name | Type | Required | Description |
|---|---|---|---|
token | string (UUID) | always | Send it in the submission's files under the same field key. |
name | string | always | The stored file name: the visitor's file name without any path or control characters, at most 150 characters before the extension, with the extension its content proves (jpg, png, webp, heic or pdf). file when nothing is left of the name. |
size | integer | always | The file size in bytes. |
{
"data": {
"token": "9b2f0c3e-5a1d-4f7e-8c2b-1d3e4f5a6b7c",
"name": "brief.pdf",
"size": 482113
}
}Tokens
- A token belongs to the form and the field it was uploaded for. Sent for another form or another field, the submission answers
422onfiles.{key}. - A token works for one submission. A submission that is rejected (any
422) claims nothing, so the same tokens can be sent again with the corrected submission. - A token expires 24 hours after the upload. An expired token answers
422onfiles.{key}; upload the file again. Files that no submission claimed are deleted once their token has expired. - Upload one file per request. For a field with
max_filesabove 1, upload each file and send all tokens in one list, at mostmax_files. - The upload does not look at the form's rules: whether the field is shown is decided when the submission arrives. Tokens for a field that is hidden at that moment are ignored and expire unused.
Upload limit
Each form accepts 30 uploads per visitor per hour, counted per visitor address from X-Borkol-Visitor-Ip, separately from the submission limit. Every attempt counts, including uploads that fail with 422. Over the limit:
{
"message": "Too many uploads. Try again later."
}The response has status 429 and carries no Retry-After header. The store-wide limit of 120 requests per minute applies as well (see Errors and rate limits).
Errors
422 validation. The standard envelope (see Errors and rate limits):
| Key | Message | When |
|---|---|---|
field | Laravel's standard messages | Missing, not a string or longer than 64 characters. |
field | Not a file field of this form. | No file field with that key in the published version. |
file | Laravel's standard messages | Missing, not an uploaded file, or larger than 20 MB. |
file | The file may not be larger than {max} MB. | Larger than the field's max_mb. |
file | This file type is not accepted here. | The content is not a type the field's accept allows. |
{
"message": "This file type is not accepted here.",
"errors": {
"file": ["This file type is not accepted here."]
}
}Other errors.
404when no form of the store has that key, or it is a draft, archived, never published or deleted.403{"error":"Module not enabled: forms"}when the forms module is off for the store, and403{"message":"Invalid ability provided."}when the key lacksforms:write.401as on every/v1/apiroute.