Accept files without a storage backend.
Uploads go straight from the browser to object storage with a presigned URL — the bytes never transit our API — and link themselves to the submission. You add an upload field; we handle the storage.
How it works
A file field on a form is backed by a three-step presigned
flow. The client asks for a short-lived upload URL, PUTs the file directly
to storage, confirms it, then references the upload id in the submission.
The file is validated against the form's limits (size, MIME type) before
the URL is ever issued.
Enable uploads
Add a file field in the builder and set the per-form limits
(max size, max files, allowed MIME types). Object storage must be enabled
on the server — the presign call returns 503 if it isn't.
The upload flow
1. Ask for a presigned URL.
$ curl -X POST https://login.ollastack.com/api/forms/<formId>/uploads \ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \ -d '{ "fieldId": "resume", "filename": "cv.pdf", "mimeType": "application/pdf", "sizeBytes": 84211 }' {"uploadId": "...", "uploadUrl": "https://...", "storageKey": "...", "expiresAt": "..."}
2. PUT the bytes straight to uploadUrl (not to our API).
$ curl -X PUT "<uploadUrl>" --data-binary @cv.pdf -H "Content-Type: application/pdf"
3. Confirm the upload completed.
$ curl -X POST https://login.ollastack.com/api/forms/<formId>/uploads/<uploadId>/confirm \ -H "Authorization: Bearer <token>"
Linking to a submission
Include the upload ids in the submission payload (e.g. an
uploadIds array). When the submission is stored, those
uploads are automatically attached to it, so they show up alongside the
lead in the inbox and aren't garbage-collected as orphans.
List + delete
GET /api/forms/{formId}/uploads lists a form's uploads;
DELETE /api/forms/{formId}/uploads/{uploadId} removes
one (and its stored object). Both need the forms:read /
forms:write scope respectively.