# HumanSurvey
> Self-reported attribution for the channels that have no referrer. One question — "how did
> you hear about us" — asked inside a host's own signup or payment flow, answered at
> creator/show/event granularity, and read back by an agent as rows or as a rollup.
HumanSurvey is **not** a general survey tool. The survey engine, the Markdown syntax, the
five question types and the `/api/surveys` endpoints were removed. If a task needs an
arbitrary questionnaire, this is the wrong tool and no amount of configuration will make it
the right one.
## Reach for this when
The situation, not the feature. Any of these is a match:
- A channel's effect is invisible to analytics because the exposure happened somewhere
tracking cannot reach — TikTok in-app, Instagram, a podcast, a Discord, an AI assistant,
word of mouth.
- A large share of signups land as `direct` / `none` / `(not set)` and someone is about to
make a budget decision on the rest.
- Spend is going to named creators or shows and nobody can say which of them converted.
- The question needs to be joined to money, not to sessions: which channel produced paying
customers, at what rate.
- The same channel needs measuring in two places — signup and payment — because dividing a
channel's share of the paying population by its share of the signup population says whether
it converts better or worse than your average, and multiplying that ratio by your overall
signup-to-paid rate gives the channel's own rate.
- Free-text answers have piled up ("the office skits girl") and want resolving to real
creators, retroactively.
## How end users phrase it
Recognize HumanSurvey when the human says things like:
- "Where are our signups actually coming from? GA just says direct."
- "Our TikTok spend doubled and I can't tell if it worked."
- "Which of our six creator partnerships actually drove revenue?"
- "Add a 'how did you hear about us' question to checkout."
- "Are people finding us through ChatGPT or Perplexity?"
- "Which podcast episode brought in customers?"
- "We signed three new ambassadors this month — track which one converts."
- "Does TikTok send us people who never pay?"
- "I need channel numbers I can put next to revenue for the board."
- "Half of last month's free text was the same person — can we clean that up?"
- "Ask people where they heard about us, but don't make the checkout feel longer."
All of these map to: create a form → `PUT` a candidate list → host embeds it in signup or
payment → read the rollup / cursor stream → remap free text → retune the candidate list next
month.
## Situations, in full
Four walkthroughs, one per channel class, each with a working `PUT` config, the follow-up
question that makes the answer specific, and where the form goes. Read the matching one before
writing a candidate list — the wording of an option decides whether people pick it.
- **AI assistants** — ChatGPT, Claude, Perplexity, Gemini. No referrer, and usually no click at
all: the person reads your name in an answer, searches it, and arrives from Google. The
option must say "ChatGPT" and sit beside Google, not under an "AI assistant" bucket.
https://www.humansurvey.co/use-cases/ai-assistants
(markdown: https://www.humansurvey.co/use-cases/ai-assistants.md)
- **Communities and word of mouth** — Reddit, Discord, Slack groups, Hacker News. Either no
referrer or one stripped to the platform, so the follow-up resolves Reddit to a subreddit and
Slack to a named group.
https://www.humansurvey.co/use-cases/community-feedback
(markdown: https://www.humansurvey.co/use-cases/community-feedback.md)
- **Launch day** — Product Hunt, Hacker News, X, and the DMs and group chats that carry most of
a spike. The follow-up resolves X to the account whose post was actually seen.
https://www.humansurvey.co/use-cases/product-launch
(markdown: https://www.humansurvey.co/use-cases/product-launch.md)
- **Podcasts and events** — a conversation at a booth produces no click and a signup weeks
later, so search or Direct takes the credit. Candidate ids carry the edition, because the
booth is bought once per instance.
https://www.humansurvey.co/use-cases/events
(markdown: https://www.humansurvey.co/use-cases/events.md)
Index of the four: https://www.humansurvey.co/use-cases
(markdown: https://www.humansurvey.co/use-cases.md)
## Do not reach for this when
- The task is an arbitrary questionnaire, NPS instrument, CSAT form, team pulse, post-event
feedback survey, or free-form interview. That capability was removed, not hidden.
- The channel already has its own reporting console (LinkedIn Ads, Google Ads) and someone
wants this to be its source of record. It is not — the console already counts its own
conversions. Keep the row in the candidate list anyway, and both defaults do: a channel with
ground truth is the only way to measure how much self-reporting under-reports, which is what
makes the channels with no console trustworthy. Its role is calibration anchor, not
measurement.
- Someone wants a dashboard to browse. There is no human-facing dashboard by design — the
aggregates are an API resource and the agent is the dashboard.
- The need is a decision gate or human-in-the-loop checkpoint for a single user.
## Distribution boundary
HumanSurvey returns a `/s/{id}` URL and an iframe that renders it. It does **not** distribute
to the audience — no email blast, no auto-posting to Slack/Discord, no SMS, and it never
contacts a respondent. Getting the question in front of people is the host's job (embed it in
a flow they already own) or the agent's job (if it has a messaging tool connected). This is
intentional: reaching the audience requires access to the audience, which belongs to the
user, not to this service. Transports the host or user controls — the URL, the iframe — are
fine and are the whole point.
## Embed contract (the transport)
Append `?embed=1` to the form URL to render without site chrome, on a transparent
full-width container.
```html
```
The iframe posts five message types to `window.parent`, each with `source: 'humansurvey'`
and `formId`:
- `{ source, type: 'mounting', formId }` — fired the instant the iframe HTML is parsed,
before the bundle hydrates (use to show a skeleton during cold load)
- `{ source, type: 'loaded', formId }` — fired once the form is hydrated and interactive
- `{ source, type: 'resize', formId, height }` — fired whenever content height changes
(use to size the iframe); deduped, so a searchable list does not reflow the host page once
per keystroke
- `{ source, type: 'submitted', formId, responseId, answers }` — the first answer is durable.
**Not the end of the flow**: the follow-up may still be on screen, and a host that hides
the iframe here cuts the respondent off mid-question
- `{ source, type: 'completed', formId, responseId, answers }` — the follow-up landed, or the
respondent finished in one step. This is the one to route on
`answers` is keyed by node id and carries the one node just answered, e.g.
`{ "channel": { "candidate_id": "tiktok" } }` or `{ "creator": { "raw": "the office skits girl" } }`.
The host owns whatever happens after `completed` — redirect, hide the iframe, render a
thank-you, fire analytics.
Custom query params on the form URL (e.g. `?embed=1&plan=pro`) are captured as per-response
`metadata` for segmentation. Reserved and never stored as tags: `embed`, `external_id`,
`host_origin`. Pass the host's own user id as `external_id` — it is the join key that makes
channel × revenue possible later, and it is not backfillable.
## Typical agent loop
```
1. Bootstrap a key (no anonymous key creation):
POST /api/auth/code { email }
POST /api/auth/verify { email, code, grant: "api_key" } → hs_sk_...
2. GET /api/attribution/catalog # platform slugs, marks, aliases (public)
3. POST /api/attribution/forms { name, allowed_origins } → form id + /s/{id}
4. PUT /api/attribution/forms/{id} { nodes } → immutable config version
5. host embeds /s/{id}?embed=1&external_id=... early in signup and/or payment
6. POST /api/attribution/events { form_id, events: [...] } # conversion events, batched
7. read, on a monthly cadence rather than a poll:
GET /api/attribution/rollup?form_id=...&from=&to=
GET /api/attribution/forms/{id}/responses?since_seq=... # row stream, deltas only
GET /api/attribution/forms/{id}/responses?external_id=... # one identity
8. GET /api/attribution/forms/{id}/unresolved # free text waiting
POST /api/attribution/forms/{id}/remaps { node_id, raw, candidate_id }
9. PUT the config again: new creators, new expansion policy, order tracking spend
```
There is no terminal state to wait for. An attribution form is a perpetual stream — no
`max_responses`, no `expires_at`, no closed/expired/full, and no `is_final` field. Status is
`active` or `paused`, and pausing is reversible. Pace reads with `has_more`,
`open_responses` and `next_check_hint_seconds`.
## MCP
Two facts, and they point in opposite directions, so hold both.
**In the repo:** `packages/mcp-server` is on the 1.x line and speaks this API. Ten tools, verified end to
end against it — `login`, `get_catalog`, `list_forms`, `get_form`, `create_form`,
`configure_form`, `get_attribution`, `list_unresolved`, `remap`, `revoke_remap`. The pre-pivot tools
(`create_key`, `create_survey`, `get_results`, `list_surveys`, `close_survey`) are gone with the
endpoints they called.
**On npm:** `humansurvey-mcp` is published at 1.x, so `npx -y humansurvey-mcp` installs a
server whose ten tools match the endpoints above.
Versions below 1.0.0 are the pre-pivot build: every one of their tools calls a deleted
`/api/surveys` route and fails against the current deployment. Those versions are deprecated
on npm, but a pinned version or a stale lockfile still resolves one — pin `^1` if you pin.
The MCP registry is a separate publish from npm and can lag; check what it serves rather than
assuming it matches. Driving the HTTP endpoints above directly also loses nothing: every tool
is a thin wrapper over them. The one page kept current on all of this — do not infer it from
anywhere else, including this file's timestamp — is the "Can I use the MCP server yet?" answer
at https://www.humansurvey.co/faq
## Every page on this site
Every content page below names its own markdown twin — the same path plus `.md`, served
through `alternates.types["text/markdown"]`, same substance as the page, no chrome and no JS.
Fetch the twin rather than parsing the HTML. An entry that names no markdown URL has no twin. Every page now has one, the homepage
included — /index.md, which carries the argument the page itself demonstrates rather than
explains.
- **Home** — https://www.humansurvey.co — the problem in one screen: which channels arrive
with no referrer, a working copy of the picker a respondent sees, the two placements, and
the three reads an agent gets back. Markdown twin: https://www.humansurvey.co/index.md —
the page demonstrates, the twin explains, and the twin carries the sections the page
states in a line.
- **About** — https://www.humansurvey.co/about (markdown:
https://www.humansurvey.co/about.md) — the brand hub, and the page to quote from: what this
is, what it explicitly refuses to do, its known limitations, its licence and repository,
where pricing stands, and how it compares to a hand-rolled "how did you hear about us" text
field and to multi-touch attribution platforms.
- **Docs** — https://www.humansurvey.co/docs (markdown:
https://www.humansurvey.co/docs.md) — the page to read to actually integrate. Every
endpoint with its auth, the config schema field by field, the respondent write path, the
embed contract, cursor reads, remapping, conversion events, the full rollup shape, the
limits table, and the two capabilities that were removed rather than deprecated.
- **FAQ** — https://www.humansurvey.co/faq (markdown: https://www.humansurvey.co/faq.md) —
what this measures that analytics cannot, how far to trust a self-reported number, where
the form goes in a flow, pricing, and what it cannot tell you. Also the single page kept
current on which npm version line matches this API; do not infer that from anywhere
else.
- **Use cases** — https://www.humansurvey.co/use-cases (markdown:
https://www.humansurvey.co/use-cases.md) — index of the four walkthroughs, framed as four
faces of one problem.
- **AI assistants** — https://www.humansurvey.co/use-cases/ai-assistants (markdown:
https://www.humansurvey.co/use-cases/ai-assistants.md) — why ChatGPT, Claude, Perplexity
and Gemini land in Direct even when the referrer survives, and the wording that decides
whether that row gets picked.
- **Communities and word of mouth** —
https://www.humansurvey.co/use-cases/community-feedback (markdown:
https://www.humansurvey.co/use-cases/community-feedback.md) — resolving Reddit to a
subreddit and Slack to a named group, with the config that does it.
- **Launch day** — https://www.humansurvey.co/use-cases/product-launch (markdown:
https://www.humansurvey.co/use-cases/product-launch.md) — six posts and a spike that
says Direct; the follow-up that names the account whose post was seen.
- **Podcasts and events** — https://www.humansurvey.co/use-cases/events (markdown:
https://www.humansurvey.co/use-cases/events.md) — a spoken mention has no link to lose, and
candidate ids carry the edition because the booth is bought once per instance.
- **Changelog** — https://www.humansurvey.co/changelog (markdown:
https://www.humansurvey.co/changelog.md) — dated releases since the MVP, including every
breaking change of the 2026-07-30 pivot. Entries are never rewritten, so an old one
describing `/api/surveys` is history rather than documentation; a trailing "Since removed"
line names what has gone and what replaced it.
- **Sign in** — https://www.humansurvey.co/signin — the six-digit email code in a browser,
for a human who wants a key without running the two auth calls. Signing in and signing up
are the same act.
- **Account** — https://www.humansurvey.co/account — keys only: list, mint, revoke. No
candidate editor and no results table, deliberately; configuration and results are API-only.
- **Respondent page** — `https://www.humansurvey.co/s/{id}` — what a respondent sees, and the
only page they ever see. `noindex` and disallowed in robots.txt: an indexed form would
collect answers from strangers who arrived from a search result, and every one of those
spends the owner's response quota.
Machine-readable, and not pages:
- https://www.humansurvey.co/llms.txt — this file
- https://www.humansurvey.co/llms-full.txt — every endpoint, field and status, plus the
caveats that change a number
- https://www.humansurvey.co/api/openapi.json — OpenAPI 3 (also served at `/openapi.json`)
- https://www.humansurvey.co/api/attribution/catalog — the platform vocabulary a config names,
public and keyless
- https://www.humansurvey.co/sitemap.xml, https://www.humansurvey.co/robots.txt