Skip to main content

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.

identify your user
"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().

DOM / headless
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.

compute the hash server-side
// 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 hash
// 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. Set resumeUrl to 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

Was this page helpful?