Prefill automatically from the URL, pass initialData from the app, carry campaign parameters in hidden fields, or call the parser directly.
URL prefill
URL prefill works in embeds and on hosted forms (SDK 0.6+). After the page loads, the SDK reads its query string. Supported fields use ?fieldId=value. Hidden fields use their configured paramName, or the field ID when no parameter name is set.
Supported URL values follow the live parser rules:
- Short text, long text, email, URL, phone, date, and hidden fields: Use the parameter value as text. Visible fields still go through normal validation before submission.
- Number fields: Accept plain decimal values within the configured minimum and maximum. Exponents, hexadecimal values, and non-finite numbers are ignored.
- Rating and linear scale fields: Accept whole numbers within the field's configured range.
- Checkbox fields: Treat true, 1, and yes as checked. Any other non-empty value is unchecked.
- Select and dropdown fields: Accept an exact option ID and ignore values that are not configured options.
- Multi-select fields: Accept comma-separated exact option IDs without added spaces. Unknown IDs and duplicates are discarded, and at least one valid option must remain.
Empty parameter values are ignored. Choice and numeric values outside the schema are also ignored. Visible text-like values still go through normal form validation before submission. file_upload, ranking, matrix, signature, custom, calculated fields are never prefilled from a URL.
Build a share link
Build prefill links with URL and searchParams so every value is encoded. Choice values are option IDs, not labels.
// Visible fields match their field id. Hidden fields match their paramName.
const url = new URL("https://fillo.so/f/your-form");
url.searchParams.set("email", "ada+demo@example.com");
url.searchParams.set("plan", "team"); // an option id, not the option label
url.searchParams.set("src", "partner newsletter"); // hidden field with paramName "src"
console.log(url.toString());
// https://fillo.so/f/your-form?email=ada%2Bdemo%40example.com&plan=team&src=partner+newsletterThe same parameters work on the hosted page and on any page that renders the form with the SDK. Anyone can edit a query parameter, so use prefill for convenience and attribution, never for access control, pricing, or identity.
initialData
Precedence: an explicit initialData prop and anything the visitor already typed always win over URL parameters.
"use client";
import { FilloForm } from "@usefillo/react";
export function Onboarding({ userEmail }: { userEmail: string }) {
// initialData wins over URL parameters; visitors can still edit the values.
return <FilloForm formId="onboarding" initialData={{ email: userEmail, plan: "team" }} />;
}Prefill from a signed-in session
Pass values the signed-in person already gave you through initialData. They stay editable. To record who answered, pass the respondent prop, and add a server-computed hash when identity has to be trusted. Never infer identity from a prefilled email field. See Respondents for the identity and verification contract.
Campaign attribution (UTM)
Add a hidden field for each campaign parameter and set paramName to the query key. The hosted page and the SDK read them from the URL automatically.
import { defineForm } from "@usefillo/react";
export const demoRequest = defineForm({
id: "demo-request",
title: "Request a demo",
pages: [{
id: "request",
blocks: [
{ id: "email", kind: "email", label: "Work email", required: true },
{ id: "utm_source", kind: "hidden", label: "UTM source", paramName: "utm_source" },
{ id: "utm_medium", kind: "hidden", label: "UTM medium", paramName: "utm_medium" },
{ id: "utm_campaign", kind: "hidden", label: "UTM campaign", paramName: "utm_campaign" },
],
}],
});When the form mounts later in a product flow, the landing URL is gone. Save the values on arrival and pass them through initialData.
const UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign"] as const;
// On the landing page: keep the campaign values for this browser tab.
const params = new URLSearchParams(location.search);
for (const key of UTM_KEYS) {
const value = params.get(key);
if (value) sessionStorage.setItem(key, value);
}
// Later, where the form mounts: pass only the values you saved.
const initialData: Record<string, string> = {};
for (const key of UTM_KEYS) {
const value = sessionStorage.getItem(key);
if (value) initialData[key] = value;
}
<FilloForm formId="demo-request" initialData={initialData} />;UTM values are respondent-supplied context. People, link shorteners, and referrers can change or strip them, so don't base billing, commissions, or access on them. Report a missing source as direct or unknown instead of guessing one.
Manual parsing
Defaults: hidden fields may carry a defaultValue, applied when no matching parameter is present. A date field may set defaultToday: true to start on the respondent's own date; a ?date_id=YYYY-MM-DD parameter still wins. Other field kinds do not take defaultValue. Schema normalization strips it, so use initialData for visible fields.
For manual control, @usefillo/core exports the parser itself:
import { defineForm, prefillFromParams } from "@usefillo/core";
const form = defineForm({
id: "waitlist",
pages: [{ id: "p1", blocks: [
{ id: "email", kind: "email", label: "Email", required: true },
{ id: "src", kind: "hidden", label: "Source", paramName: "src", defaultValue: "direct" },
]}],
});
// The exact parser the SDK runs after hydration. Call it yourself when you
// want the values earlier, or somewhere without a URL:
const params = Object.fromEntries(new URLSearchParams("?email=ada@example.com&src=newsletter"));
const initialData = prefillFromParams(form.schema, params);
// Result: { email: "ada@example.com", src: "newsletter" }, coerced and validated per kindRelated
- Respondents: Verify identity instead of trusting prefilled answers.
- Hidden fields: Carry attribution and app context.