Respondents & identity
Identify signed-in users, save unfinished answers, and let people return to edit a response. Requires @usefillo/* 0.7.0 or newer.
Identify who's filling the form
Pass your signed-in user with the respondent prop. Every response that person submits arrives with their id, email, and name attached — in the responses grid, the response drawer, webhooks, Zapier, and CSV export — and a person view filters everything one respondent ever submitted. respondent.id is your own user id; it’s the identity key within your project.
"use client";
import { FilloForm } from "@usefillo/react";
// Pass your signed-in user. Every response this person submits now arrives
// with their id, email, and name attached — in the responses grid, webhooks,
// Zapier, and CSV export. Safe to pass late (after your session loads).
export function NpsForm({ user }: { user: { id: string; email: string; name: string } }) {
return (
<FilloForm
formId="nps"
respondent={{ id: user.id, email: user.email, name: user.name }}
/>
);
}The same option works on <FilloProvider>, the framework-free renderForm() (with setRespondent() to bind late), and createFormController().
import { renderForm } from "@usefillo/dom";
// The DOM renderer accepts the same respondent context as the React renderer.
const form = renderForm("#form", {
formId: "nps",
respondent: { id: user.id, email: user.email, name: user.name },
});
// Late-bind it once your session resolves:
form.setRespondent({ id: user.id, email: user.email, name: user.name });By default identity is an unverified claim supplied by the page. It can label and group responses, but it isn’t an authentication boundary: page code can claim any id. Turn on verification before identity affects anything you need to trust, especially an update-in-place limit.
Verify identity
In Fillo → Settings → Developers → Identity verification, generate a project secret. From then on, Fillo records a respondent only when the submission carries a valid HMAC-SHA256(userId, secret) computed on your server — so a script on the embedding page can’t claim to be another user. Verified responses are marked as such in the grid, drawer, and payloads.
// On YOUR server — never in the browser (the secret must not ship to clients).
// Copy the secret from Fillo → Settings → Developers → Identity verification.
import { createHmac } from "node:crypto";
export function respondentHash(userId: string): string {
return createHmac("sha256", process.env.FILLO_IDENTITY_SECRET!)
.update(userId)
.digest("hex");
}Then pass it with the identity:
// Pass the server-computed hash alongside the identity. Once your project
// has a secret, Fillo records identity ONLY when the hash validates, so a
// script on the page can't impersonate or enrich another user's profile.
<FilloForm
formId="nps"
respondent={{ id: user.id, email: user.email, hash }}
/>Never compute the hash in the browser — that would expose the secret. If verification is on and no valid hash arrives, the response is still recorded, just anonymous.
Hold unverified submissions
On a form that lives behind sign-in — in-product feedback, member surveys, account requests — every real response comes from a user your server can vouch for. Once verification is set up, turn on Hold unverified submissions for review in the form’s Respondents settings. From then on, a submission without a valid hash doesn’t go anywhere: it’s held for your review instead.
A held submission is stored, but nothing is delivered or shown. No webhooks, no Google Sheets, Notion, or Slack, no notification or receipt emails, no Zapier — and it stays out of exports, analytics, and response counts. The submitter sees the same confirmation as everyone else; nothing tells a script its submission was held.
Held submissions wait under Held on the form’s responses page. Release the legitimate ones — a released response is delivered everywhere as if it had just been accepted, one response.created event carrying the original submission time — and delete the rest.
Leave this off on forms with anonymous traffic: a shared link, a public page, anywhere respondents aren’t signed in. There, every genuine response would be held too. The dashboard setting requires the project identity secret. A code-defined form can stage settings.trust before the secret exists, but the policy is fail-closed: every submission is held until verification is set up or the policy is removed.
Save and resume
Turn on Save and resume in a form’s Respondents settings. The SDK then autosaves in-progress answers as the visitor types and restores them when they return — after a reload, a closed tab, or (for a verified respondent) on another device. A “picked up where you left off” notice appears with a Start over action. Unsubmitted drafts are deleted on submit, on start over, or automatically 7 days after the last change. It’s a per-form setting the renderer honors — no code changes. Headless UIs can read resumedDraft and call resetDraft() / flushDraft().
Recover drop-off
Once a form saves progress, four more per-form settings turn abandoned starts into recoverable ones. All are off by default, and none carry answer content off the platform.
Configure these recovery, email, and pre-submission visibility controls in the dashboard. Public-key code sync preserves them and ignores incoming values for draftAnswersVisible, resumeEmails, resumeUrl, and draftDigest.
- In-progress counts — how many people are mid-fill (and how many identified), on the form overview, in insights, and on the responses header. No opt-in beyond Save and resume.
- See in-progress answers (
draftAnswersVisible) — an “In progress” tab where your team reads what someone typed so far and helps them finish. It exposes pre-submission content to your workspace, so it’s a separate opt-in with its own consent copy — disclose it in your privacy policy. The answers show only in the dashboard; the draft token stays the only public read capability. - Resume emails (
resumeEmails) — one “pick up where you left off” link when someone leaves a form idle for a day, to their verified account email or an address they entered, at most once per draft, never with answers. SetresumeUrlto land it inside your product. - Drop-off digest (
draftDigest) — a daily summary to your notification address: who abandoned the form and where they stalled, verified names only, counts not content.
Your app can also subscribe a webhook to the draft.abandoned event (opt-in per webhook) to run its own nudges — a signal payload with the verified respondent id where known, never answer content.
One response per person
One Limit responses setting in a form’s Respondents settings decides who counts as the same responder and what happens when they return. Pick “one response per”:
- Browser: same device, anonymous. Best for ratings, polls, and “was this helpful”; no sign-in or details needed.
- Email or Phone: a contact field they fill in. Recognized by their answer, so it’s a soft self-claim, not a guarantee. Make the field required so every response is counted.
- Signed-in user: the
identify()respondent supplied by your embed. Without verification it’s still a page-supplied claim; with a project secret and valid hash it becomes verified. A keep-first limit rejects a submission it can’t identify. An anonymous shared-link submission can’t match a previous person; in update mode it’s recorded as a new response instead of overwriting one.
Optionally add “…per” a field, such as an article or product id, so one shared form dedupes once per article instead of once overall. On a repeat, either keep their first answer, or (Signed-in user only) let them update it in place, which re-anchors to the current form version, emits a response.updated event. Reading and prefilling the person’s previous answers requires a verified identity.
Email or Phone remains a soft self-claim and is suitable only for basic duplicate reduction; the project identity secret doesn’t verify a contact-field answer. Signed-in user is also unverified until you configure the secret and pass a valid server-computed hash. Don’t use update-in-place for a security- or entitlement-sensitive workflow without verification.
Erase a person (GDPR)
From the person view (filter the responses grid to one respondent), Forget person removes their profile, drafts, and identity across this project while leaving the answers anonymous; Delete person + responses also deletes their responses and the uploaded files in your connected storage. Both act project-wide, because identity is project-level.
This page for agents: /docs/respondents.md · index at /llms.txt
Updated