# Respondents

Last updated: 2026-07-07

Canonical human page: https://fillo.so/docs/respondents
Quickstart: https://fillo.so/docs/embed.md
Code-defined forms: https://fillo.so/docs/authoring.md
Styling: https://fillo.so/docs/styling.md
Prefill: https://fillo.so/docs/prefill.md
Webhooks: https://fillo.so/docs/webhooks.md
Custom UI: https://fillo.so/docs/custom-ui.md
API reference: https://fillo.so/docs/reference.md
Troubleshooting: https://fillo.so/docs/troubleshooting.md

Because a Fillo form renders inside your product, it can behave like your product: remember progress, recognize the signed-in user, and let people return to edit. These are the respondent features — `identify()`, save & resume, and person-keyed responses. They need `@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 — visible 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 is the identity key within your workspace.

```tsx
"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()`.

```ts
import { createFormController } from "@usefillo/core";

// The respondent option works everywhere the engine does — the DOM renderer
// (renderForm), the headless controller, and <FilloProvider> in React.
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 from the page — fine for enriching your own dashboards. Turn on verification when you rely on it.

## Verify identity

In Fillo → Settings → Developers → Identity verification, generate a workspace 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:

```ts
// 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:

```tsx
// Pass the server-computed hash alongside the identity. Once your workspace
// 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.

## 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. No code changes — it's a per-form setting the renderer honors. Headless UIs can read `resumedDraft` and call `resetDraft()` / `flushDraft()` (see the [API reference](https://fillo.so/docs/reference.md)).

## One response per person

Two per-form settings, both keyed on the identified respondent:

- **Reject repeats** (`submissionLimit: "once_per_person"`) — one response per person; a repeat submit returns the standing response. Anonymous submits are rejected, so the form must be embedded with `identify()`.
- **Update their response** (`responseMode: "upsert"`) — the same person edits their response in place instead of creating duplicates. It re-anchors to the current form version, emits a `response.updated` webhook event, and — for a verified respondent — prefills their previous answers for editing. This turns a one-shot form into a living record: a profile, a preference center, a recurring check-in.

Both are unenforceable without identity, so turn on verification before you rely on them for anything that matters.

## Erase a person (GDPR)

From the person view (filter the responses grid to one respondent), **Forget person** removes their profile, drafts, and identity across the workspace while leaving the answers anonymous; **Delete person + responses** also deletes their responses and the uploaded files in your connected storage. Both act workspace-wide, because identity is workspace-level.
