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 Markdown

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

NameTypeRequiredDescription
keystringYesThe form key, for example quote-request.

Headers

NameTypeRequiredDescription
AuthorizationstringYesBearer {forms_write_key}.
Content-TypestringYesmultipart/form-data (set by your HTTP client when you send form data).
X-Borkol-Visitor-IpstringNoThe 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.

NameTypeRequiredDescription
fieldstringYesThe key of a file field in the form's published version, at most 64 characters.
filefileYesOne 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).
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"
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. The result is wrapped in data.

NameTypeRequiredDescription
tokenstring (UUID)alwaysSend it in the submission's files under the same field key.
namestringalwaysThe 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.
sizeintegeralwaysThe file size in bytes.
Response
{
  "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 422 on files.{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 422 on files.{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_files above 1, upload each file and send all tokens in one list, at most max_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:

Response
{
  "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):

KeyMessageWhen
fieldLaravel's standard messagesMissing, not a string or longer than 64 characters.
fieldNot a file field of this form.No file field with that key in the published version.
fileLaravel's standard messagesMissing, not an uploaded file, or larger than 20 MB.
fileThe file may not be larger than {max} MB.Larger than the field's max_mb.
fileThis file type is not accepted here.The content is not a type the field's accept allows.
Response
{
  "message": "This file type is not accepted here.",
  "errors": {
    "file": ["This file type is not accepted here."]
  }
}

Other errors.

  • 404 when 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, and 403 {"message":"Invalid ability provided."} when the key lacks forms:write.
  • 401 as on every /v1/api route.