This is the long version. It is written for a security review, an architecture conversation or a diligence request, and it goes further than the public site does on purpose. Please do not redistribute it.
How it works
A person proves they are real once. Then nobody has to ask again.
Here is the whole process, including the parts most vendors leave out: where the handoff happens, what we keep, what we refuse to keep, and why we designed it that way. If you are on a security review, everything on this page is the same answer we give in a questionnaire.
About 2 minutes
Start to verified, for the person doing it. No app install required.
A name, a signal, a face
The fields reviewers ask about first: a name, an 18-plus signal, a status, one still photo, and an email. No birth date, and no faceprint from this pipeline. This is not the complete field list, which is below and longer.
A category, not a document
What a relying party receives. Never a reason, never a document, never an image.
The process
Five steps, and only one of them involves the person.
This is the school-initiated path, which is the one procurement asks about. A person claiming their own badge in the app skips step one and starts at step two.
Status, as of September 2026, because you are going to ask and we would rather answer first. The consumer product is live in production on iOS and on the web, with Android in testing. This school-initiated partner path is built, deployed and configured, and it has not yet been exercised by a live institutional partner. Everything below describes running code and provisioned infrastructure rather than a roadmap, and you would be the first through it. We will say that in a meeting too, so it may as well be in writing.
1
The institution asks, in one call.
The relying party sends a request against one endpoint. No new system, no hardware, no file transfer, no roster upload. There are two partner surfaces and each is a single call: this one starts a verification for an applicant you are asking about, and GET /public/verification/outcome asks whether a person is already verified.
Be exact about what crosses the wire: the applicant’s email address, in the clear, and their name if you send it. Both endpoints identify a person by email. We hold that email and name on the request row, and on the audit export you can pull. Be exact about the retention too: that row has no purge job today, so it persists after the request resolves, for a person who may have no Fuzzy account at all. A scoped retention window is open work. And a relying party cannot query us by hash today, so we are not going to describe that as a privacy property until it is one.
One primitive serves every vertical. A university verifying a flagged applicant and a marketplace verifying a seller write the same kind of record, differing only in type and context. That is why integration is measured in days rather than quarters.
Where the hash actually lives. The immutable audit record does not carry the email. It carries a peppered HMAC of it, so that erasing the request row holding the cleartext identity never severs the proof that a check happened. The hash is a retention design, not a wire format.
2
Fuzzy opens a session with Incode and returns the link to you.
Fuzzy calls Incode to start a hosted web session, gets back a flow URL, and returns that URL to the institution in the API response. The institution delivers it to the applicant through the channel it already uses: its own email, its own SMS, or a link in its own applicant portal. Fuzzy does not message the applicant. We send nothing to a person who is not our member, which is also why no consent or suppression obligation for your applicants lands on us.
The link is deliberately short-lived. Incode expires a hosted flow URL within minutes, so re-reading the request returns a freshly minted link rather than a dead one. Integrate against the status read, not against a URL you stored an hour ago.
At the end of the flow Incode returns the applicant's browser to a Fuzzy callback, which forwards them to redirect_url if you supplied one, and otherwise shows a plain Fuzzy completion page.
This is the handoff, and it is deliberate. Fuzzy does not build identity verification. Incode does that, and does it to a standard we could not match alone. Fuzzy is the credential layer on top.
Two reasons. The applicant never has to install anything: the capture runs in a browser on whatever device they already have, and a school cannot mandate an app download for financial aid. And the institution keeps its own channel to its own applicant, which means the message arrives with your deliverability and your branding rather than from a vendor the student has never heard of.
3
The person proves they are live.
A government ID capture and a live selfie check, run inside Incode's hosted flow. The liveness test is what separates this from a photo upload: it is checking that a real human is present right now, not that a picture exists.
The ID document is never stored in our database, and never reaches the school. The document, the liveness video and any face template stays inside the verification provider, which is also where the face match itself is run. Fuzzy receives one still photo back, and that becomes the badge photo.
Whoever holds the biometrics carries the risk. By keeping that boundary at the provider, the school never becomes the custodian of a database of student faces and IDs, and neither do we. Fewer copies of sensitive data is not a feature we added, it is the architecture.
4
One outcome is written, and it is a category.
Incode's webhook tells Fuzzy the session finished. Fuzzy checks the result, runs the name match, and writes the outcome as a category with a timestamp, alongside the scores and vendor diagnostics kept for support and dispute review.
You read the outcome by polling, and we would rather say that plainly than imply a push you cannot subscribe to. On this path the institution reads GET /public/verification/full/{id} and gets the category only. A signed outbound webhook does exist in the codebase, but be clear about its limits: it is not wired to this path, it is not configured in production today, it carries three outcomes rather than five, and its interim configuration broadcasts to a global partner list rather than to the organisation that asked. Per-organisation webhook subscriptions are open work. Until they ship, treat the poll as the integration.
That record is append-only and tamper-evident. Each one is chained to the one before it with a keyed hash under a key held outside the database, and append-only is enforced by a database trigger rather than by convention. Someone with full write access to the database but no key cannot alter a record and relink the chain.
The precise version of "including by us." A hash chain alone does not bind an operator who holds both the database and the key, and we hold both. What binds us is the export receipt: every audit export is signed by an AWS KMS key our own services can sign with and cannot read. Retain your receipts and you can prove later what we told you at the time, whatever the database says by then. That is the property worth having, and it is the one we would rather you test.
Because an audit trail you can edit is not an audit trail. And because the same record serves both the compliance evidence and the billing event, there is one system to trust rather than two that can disagree.
5
The credential belongs to the human, and it travels.
The application ends. The badge does not. It is portable, it is free to the person who holds it, and one re-verification every twelve months keeps it live.
When they present it somewhere new, they choose what to share. The recipient gets only the claims the holder scoped, the token can be redeemed exactly once, and it expires.
In the data model, a credential has no owning organisation. It is deliberately not a row that belongs to the school that paid for the check. That is the difference between a verification and a credential, and it is the whole reason this company exists.
The handoff
Who touches what.
The most common question in a security review is not how it works. It is who ends up holding the sensitive material. Here is the honest answer.
The institution
RELYING PARTY
- Sends a request and receives a category
- Gets a signed, exportable audit record
- Never receives an ID document
- Never receives a face image or biometric data
- Never receives a failure reason
Nothing sensitive arrives, so nothing sensitive has to be protected, retained, or disclosed later.
Fuzzy
CREDENTIAL LAYER
- Holds the credential and its status
- Holds seven identity fields on the credential, the badge selfie, and an email
- Writes the append-only audit chain
- Never sees the ID document
- Never stores a face template or liveness video on the verification pipeline. Scope matters and we will not blur it: our legacy Watchdog product does hold face geometry, in a separate system a verified member never touches
We are the layer that remembers a person was verified. We are deliberately not the layer that does the verifying.
Incode
IDENTITY ENGINE
- Captures the ID and the live selfie
- Runs the liveness and document checks
- Returns a result, not the raw material
SOC 2 Type II, ISO 27001, ISO 30107-3 and FedRAMP Ready are Incode certifications, and the 99.6% fraudulent-document detection rate is Incode's figure. We attribute them rather than borrow them.
Data
What we keep. What we refuse to keep.
Data minimisation is not a policy paragraph here, it is a list. The right-hand column is the more important one. Read these as the fields a security review actually asks about, not as a closed inventory: a list that reads as complete and is not becomes a denial of everything it omits, which is worse than giving no list at all.
What Fuzzy stores
THE FIELDS REVIEWERS ASK ABOUT MOST
- Given name and family name
- An 18+ age band, established at verification
- Verification status and its dates
- City, region and country, taken from the document
- The still photo from verification, which becomes the badge photo, plus an email address
- A one-way keyed hash of the subject on each audit record, so the immutable ledger holds no cleartext identity. Relying parties still send us an email address to ask a question; hash-only lookup is not built
- On the partner-initiated path only: the applicant’s email and name in the clear on the request row, retained past resolution today, and reachable on your audit export
On the photo, precisely. The still image from verification becomes the badge photo and the face the verified ring wraps. It is a photograph, not a faceprint: no face template is stored for a verified member, and the match runs at Incode against Incode's own record rather than against our copy. Anyone with the badge link can load the photo, and messaging apps fetch it for link previews, which is what makes a badge readable by a person. Age is an affirmative 18-plus signal; the birth date is read at verification and not kept in the credential.
What Fuzzy never stores
BY DESIGN, NOT BY POLICY
- ID documents or any image of one
- A face template or biometric embedding from the verification pipeline, or the liveness video
- Social security numbers
- Home addresses
- ID or document numbers
- A presentation token in readable form. It is stored as a hash, so possession is the credential
Two things that belong in the other column, said here so you do not have to catch us. A failure reason is never disclosed to a partner or an institution, and that rule is real and enforced. But we do store the vendor's diagnostic fields, including its reason message and the rule it applied, for support and dispute review, and a member can see their own reason in their own app. Non-disclosure is the guarantee; non-retention is not. Separately, presentation tokens are hashed at rest but badge tokens and share-link tokens are stored in readable form, so an earlier "no token in a readable form" line was too broad.
Age is answered as an affirmative 18-plus signal and that signal is stored, as the list opposite says. What is not stored is the birth date, and what is never written is a negative: the field is only ever true or empty, never false, so there is no list of known minors at rest anywhere in the system. Under-18 is deliberately not one of our outcome states.
Outcomes
Five answers. None of them is a reason.
This taxonomy exists because Barry's admissions team asked what they would actually see, and because a school does not want to hold a database of why real people failed an identity check any more than we do.
| Outcome | What it means | What the institution does |
|---|---|---|
| verified | Completed and passed, with a verified-on date. | Clear them. No manual identity work at all. |
| expired | Was verified. The credential lapsed past its window. | Ask for a re-verification. Cheap, they are already enrolled. |
| attempted_incomplete | Started and never finished. Abandoned, or a device or access problem mid-flow. | Follow up and help them finish. These are usually recoverable people, not fraud signals. |
| attempted_failed | Completed the flow and did not pass. | Route to your own review. Failure is not proof of fraud and should never be treated as such. |
| not_attempted | No verification interaction on record. | Prompt them. Deliberately distinct from failing. |
One more state, on the school-initiated path only. A verification you started and that has not finished yet reads as pending, and it carries the applicant’s current link. That path also never returns expired or not_attempted, because it is answering about a request you made rather than about a person’s standing credential. Those two belong to the outcome endpoint. Same taxonomy, two different questions, and a school integrating against the five steps above should code for pending.
A relying party gets a category. Never a document.
And nobody is blocked for non-completion. Anyone who cannot verify gets an in-person or video path through your own office, never a dead end. That is a product rule, not a courtesy.
Design decisions
Six choices we would defend, and what is not finished about them.
Each of these is a real constraint in the system rather than an intention, and each exists because of a specific failure we did not want to be possible. Where a constraint is partially built, this says so in the same breath, because a security reviewer will find out either way and we would rather they hear it from us.
Nothing a stranger can walk.
Anything referenced from outside the system is a random identifier, not a counter. If ids are guessable, the id scheme is itself the information leak, because one valid reference gets you to the next.
Where counters do exist, and why. Ordinary lookup tables (countries, roles, services, notification types) use serial keys, because nothing external ever references them. And the audit chain deliberately carries a per-organisation sequence number, because a chain needs an order to be a chain. That number appears on your own audit export as ledger_chain_seq, scoped to your organisation, reachable only with an authenticated credential.
One identifier is shorter than the rest, and you should hear the number from us. A badge token is 7 characters, which is roughly 35 bits. It is random rather than sequential, so the claim above holds, but 35 bits is not the same order of strength as the 126-bit and 192-bit tokens elsewhere in the system. It is that short because it has to survive being printed in a watermark. The resolver that maps a token back to a person is superadmin-only, so there is no public walk from one badge to the next.
The person is not a row in your database.
A credential record carries no organisation reference at all. It cannot, structurally, belong to the institution that paid for the check. That is the credential business rather than the verification business, enforced in the schema.
The stronger version, which is also true. A paid institutional check does not write a credential row at all. It writes a verification request, tied to the requesting partner by name and explicitly not tied to a person’s account. The credential only ever comes into existence around the human. So the question "what happens to our students’ credentials if we stop paying" has a structural answer rather than a contractual one: they were never yours to stop.
An insider cannot rewrite history.
Audit records are chained with an HMAC under a key held in a secrets manager, outside the database. Someone with full write access to the database but no key cannot alter a record and relink the chain. Append-only is enforced by database trigger, not by convention.
Rotation is designed for and not yet plumbed. Records carry a key id and the code has a retired-key path, so rotating without invalidating history is possible by construction. It has not been provisioned, so there is one active key today. We would rather tell you that than let you assume a rotation schedule exists.
The holder decides what is shared.
A presentation carries only the claims the holder scoped, fixed at the moment it is created. It is redeemable exactly once, it expires, and the token is stored only as a hash, so possession is the credential.
Checking many strangers looks different from checking one person twice.
On the verification path we count distinct people queried per organisation per hour, not total calls. Re-verifying the same person repeatedly is normal. Querying hundreds of different people quickly is a harvest, and that is the shape we watch for.
What is solid here, and what is not. The distinct-subject counter is cluster-wide: its state lives in Postgres, keyed on organisation and hour, so it counts correctly no matter how many instances are running. The weaker piece is the ordinary request-rate limiting on other endpoints, which keys on client IP and holds its state in process, so its real ceiling multiplies by instance count. That one is on the list to close. An earlier version of this brief had these two the wrong way round.
The exportable record requires a credential. Less than that does not.
The exportable audit record is available only to an authenticated, partner-scoped credential. There is no way to get an institution's audit trail without being an institution we have issued a key to.
Three corrections, because an earlier version of this card overclaimed.
First, it said outcome categories and dates are auth-gated. They are not, entirely. An unauthenticated status check by email exists, throttled to 30 requests a minute per IP, and it separates the taxonomy states rather than returning a single coarse answer. An earlier correction on this card described that gap as narrower than it is. A holder-shared badge page also shows a verified-since date without a credential, which is deliberate, because the holder chose to share it.
Second, it said "per-organisation." That holds for the export, the metrics and the verification requests, which all resolve and scope to a partner. It does not hold for the outcome endpoint, where any valid key can ask about any email with no tenant scoping. Per-organisation credentials are the replacement and they are not built yet.
Third, it said "rate-limited credentials." The limiters that exist key on IP, not on credential, and the partner request and export routes carry no rate limit at all today. We would rather you read that here than find it with curl.
Still have questions? Good.
Security reviews are welcome and we would rather answer in detail than in adjectives. Tell us what your questionnaire asks and we will answer it directly.