4 fields
File upload form
Ikiform doesn't store files yet. This form collects a link to the file, and shows how to upload to your own storage first and submit the URL.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
Copy the code
Every version below is generated from the same field definition. Replace frm_xxxxxxxxxxxxxxxx with your form id, or enter it in the Form ID box and the code updates.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" type="text" maxlength="120" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="file_url">Link to the file</label>
<input id="file_url" name="file_url" type="url" required aria-describedby="file_url-help" />
<p id="file_url-help">A shared link, for example Google Drive, Dropbox or WeTransfer.</p>
<label for="notes">Anything we should know?</label>
<textarea id="notes" name="notes" rows="4" maxlength="4000"></textarea>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Send</button>
</form>- HTML needs no JavaScript. After a successful post the browser is redirected to the form's redirect URL, or to Ikiform's hosted thank-you page.
- React uses the SDK:
npm install @ikiform/sdk. Field errors from the server appear under each field. - JavaScript enhances the HTML form with
fetch, so the visitor stays on the page. - The hidden
_gotchainput is the honeypot. Leave it empty and hidden.
challenge(FORM_ID) from @ikiform/sdk and pass it to submit as turnstileToken, or add it to the fetch body as cf-turnstile-response. Without it the post is refused with 403 challenge_required. See the Next.js guide and Spam protection.The fields
Paste this definition on the form's Fields tab in the dashboard. Ikiform then checks every post against it on the server, refuses bad data with 422 validation_failed, refuses any field that isn't listed, and stores typed values.
| Field id | Type | Required | Accepts |
|---|---|---|---|
name | text | Yes | Up to 120 characters |
email | email | Yes | One valid address |
file_url | url | Yes | An https: URL |
notes | textarea | No | Up to 4,000 characters |
{
"version": 1,
"title": "File upload",
"fields": [
{
"id": "name",
"type": "text",
"label": "Name",
"required": true,
"validation": {
"max_length": 120
}
},
{
"id": "email",
"type": "email",
"label": "Email",
"required": true
},
{
"id": "file_url",
"type": "url",
"label": "Link to the file",
"required": true,
"help_text": "A shared link, for example Google Drive, Dropbox or WeTransfer.",
"validation": {
"https_only": true
}
},
{
"id": "notes",
"type": "textarea",
"label": "Anything we should know?",
"rows": 4,
"validation": {
"max_length": 4000
}
}
],
"settings": {
"submit_label": "Send",
"success_message": "Thanks, we got your file."
}
}Next.js server action
To keep the form working without JavaScript in a Next.js app, post from a server action with a secret key. Create the key under API keys with the submissions:write scope and keep it out of the browser.
IKIFORM_SECRET_KEY=ik_sk_your_secret_key"use server";
export interface FileUploadFormState {
ok: boolean;
message: string;
fields?: Record<string, string[]>;
values?: Record<string, string[]>;
}
export async function sendFileUploadForm(_previous: FileUploadFormState, formData: FormData): Promise<FileUploadFormState> {
const body = new URLSearchParams();
for (const [name, value] of formData) {
if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
}
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
},
body,
});
const result = await response.json();
if (result.ok) return { ok: true, message: "Thanks, we got your file." };
const values = Object.fromEntries([...new Set(body.keys())].map((name) => [name, body.getAll(name)]));
return { ok: false, message: result.error.message, fields: result.error.fields, values };
}"use client";
import { useActionState } from "react";
import { type FileUploadFormState, sendFileUploadForm } from "./actions";
const initial: FileUploadFormState = { ok: false, message: "" };
export function FileUploadForm() {
const [state, action, pending] = useActionState(sendFileUploadForm, initial);
if (state.ok) return <p>{state.message}</p>;
return (
<form action={action}>
<label htmlFor="name">Name</label>
<input id="name" name="name" type="text" maxLength={120} required defaultValue={state.values?.name?.[0]} />
{state.fields?.name ? <p>{state.fields.name[0]}</p> : null}
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" autoComplete="email" required defaultValue={state.values?.email?.[0]} />
{state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
<label htmlFor="file_url">Link to the file</label>
<input id="file_url" name="file_url" type="url" required aria-describedby="file_url-help" defaultValue={state.values?.file_url?.[0]} />
<p id="file_url-help">A shared link, for example Google Drive, Dropbox or WeTransfer.</p>
{state.fields?.file_url ? <p>{state.fields.file_url[0]}</p> : null}
<label htmlFor="notes">Anything we should know?</label>
<textarea id="notes" name="notes" rows={4} maxLength={4000} defaultValue={state.values?.notes?.[0]} />
{state.fields?.notes ? <p>{state.fields.notes[0]}</p> : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Send"}</button>
</form>
);
}Set it up
- Create a form in the dashboard and copy its id.
- Paste the field definition on the Fields tab.
- Add your site under Allowed domains, plus
localhostwhile you develop. - Set the rate limit to Standard, 5 a minute per visitor if visitors post from the browser. Uploads are occasional. Standard is plenty. With the server action, every post comes from your server's IP, so choose Relaxed or Custom instead.
- Paste the code, send a test and check it arrives in the dashboard.
What Ikiform does with files today
Ikiform doesn't accept file uploads yet. A post that contains a real file is refused with 422 files_not_supported; an empty file input is ignored.
| You post | Result |
|---|---|
| A form with a file selected | 422 files_not_supported |
| A form with an empty file input | Accepted; the input is ignored |
A file input with no name | Accepted; browsers don't send unnamed inputs |
A link to the file in a url field | Validated and stored |
So there are two ways to collect files. The form above asks for a shared link, which needs no code at all. Or upload the file to storage you control and submit only its URL, shown below.
Upload to your own storage first
Give the file input no name, so the browser never sends the file to Ikiform. On submit, ask your server for a presigned upload URL, upload the file there, then submit the form with the file's URL in file_url.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" type="text" maxlength="120" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="file">File</label>
<input id="file" type="file" required />
<input name="file_url" type="hidden" />
<label for="notes">Anything we should know?</label>
<textarea id="notes" name="notes" rows="4" maxlength="4000"></textarea>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Upload</button>
</form>/api/uploadsis your own endpoint. It checks the file type and size, creates a presignedPUTURL for S3, Cloudflare R2 or similar, and returns it with the URL the file will have.- Keep
file_urlin the field definition as aurlfield so Ikiform still validates it. - If the file input had a
name, the SDK would send the file and Ikiform would refuse the post with422 files_not_supported.
Where to send it
Every submission can go to a signed webhook. These guides cover the tools that suit this form, with the relay code:
- Receive form submissions on your own server
- Send form submissions to Slack
- Send form submissions to Airtable
- Send form submissions to a Notion database
Questions
Not yet. Posts that contain a file are refused with 422 files_not_supported. Collect a link, or upload to your own storage and submit the URL.
File uploads aren't built, and the whole request is limited to 256 KB. Uploading straight to object storage keeps large files off the form request entirely.
No. An empty file input is ignored, so a form with an optional file field still submits when nothing is chosen.