Forms

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.

Form ID

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

A shared link, for example Google Drive, Dropbox or WeTransfer.

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 _gotcha input is the honeypot. Leave it empty and hidden.
If you set the form's spam check to Ikiform, the HTML form keeps working with no change and the server action skips the check because it sends a secret key. The React and JavaScript versions must get a token before each submit: call 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 idTypeRequiredAccepts
nametextYesUp to 120 characters
emailemailYesOne valid address
file_urlurlYesAn https: URL
notestextareaNoUp to 4,000 characters
Field definition
{
  "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.

.env.local
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
app/file-upload/actions.ts
"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 };
}
app/file-upload/form.tsx
"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>
  );
}
A server action posts from your server's IP, so the per-visitor rate limit counts every visitor together. Use the Relaxed or Custom rate limit for forms you send through a server.

Set it up

  1. Create a form in the dashboard and copy its id.
  2. Paste the field definition on the Fields tab.
  3. Add your site under Allowed domains, plus localhost while you develop.
  4. 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.
  5. 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 postResult
A form with a file selected422 files_not_supported
A form with an empty file inputAccepted; the input is ignored
A file input with no nameAccepted; browsers don't send unnamed inputs
A link to the file in a url fieldValidated 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/uploads is your own endpoint. It checks the file type and size, creates a presigned PUT URL for S3, Cloudflare R2 or similar, and returns it with the URL the file will have.
  • Keep file_url in the field definition as a url field so Ikiform still validates it.
  • If the file input had a name, the SDK would send the file and Ikiform would refuse the post with 422 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:

Questions

Point your form at Ikiform