# Exporting users Source: https://docs.verifyyou.com/docs/activity/exporting-the-list Getting your results out as a CSV. **Export** sits on a verification's own Activity tab, and offers **Copy** or **Download**. Filter first: you export what you filtered, which is the whole point of exporting rather than reading. ## Export users to take action after the fact If you set **On collision** to *Allow + flag* instead of *Block*, acting on a result means acting in your own product, so the export is the handover: suspend the accounts, hold the payouts, or reconcile against your own records. ## Making it reconcile Export gives you a row per person. To join those rows to your own users, give us a key going in. On the code path, attach your own `external_id` when you start the session. On the no-code import, put your columns in the CSV and we keep them on the row: the product's own example is `name,email,last_seen_ip`, so the IP you have on file comes back beside the one we saw. Without either, you are matching on whatever email or phone they gave the check, which holds until somebody used a different address with you than with us. Import what you will want to compare, because you cannot add it afterwards. **Next:** [Reading Insights](/docs/insights/reading-insights) # Comprehensive trust score Source: https://docs.verifyyou.com/docs/activity/guest-profiles How we use active and passive signals to protect your platform. Clicking a row in Activity opens the person behind it. We collect two kinds of signal on every check. Active ones come from the person in front of the camera: whether the face is live, how confident we were, whether we have seen it before. Passive ones come from everything around them: the IP and where it resolves to, the device and browser, how often they have appeared and how fast. Combined, they make a trust score, and because it is recalculated on every run it moves as the person's behaviour moves rather than sitting still from the day they signed up. Trust snapshot panel showing score, claims, attributes and activity ## The score A score out of 100 sits beside every tab. Everyone starts at 100, and anything suspicious we detect during the run subtracts from it, weighted by how suspicious it is. The score shown is from this person's most recent scored pass, so it tells you about that run rather than standing as a permanent rating. Somebody who was clean last month and arrived through a datacenter IP this morning shows you this morning. Read it as a priority. A high score means we found little worth reporting. As it falls, there is more to look at, and by the time it is in the low tens there is usually a clear reason sitting one click away. What it never does is decide anything: your configuration settles the pass or fail. ## What we saw: the Signals tab The score is the summary. **Signals** is the evidence, grouped into five categories with a count against each and a filter for high, medium or low strength. Signals tab grouped into five categories **Capture** is the check itself: how many liveness attempts they made, how confident we were in the live face, and the reason if it failed. Several attempts and low confidence is the pattern behind most held-up screens and replays. **Person** is what we estimated from the scan: an age range and a gender, each with its own confidence. Estimates rather than proof, and useful mainly when they contradict what the person told you. **Location** is derived from the IP address: country, region and city. Worth holding against where you expected them, and against where they were last time. **Device** is the platform, device type and browser. One person across several devices is normal. Several people on one device is not. **Network** is usually the group that moves the score, and it is the hardest for somebody to hide. Whether the IP is a VPN, a datacenter, a Tor exit or already known for abuse, and whether the connection behaves like the browser it claims to be, which covers TLS automation, a TLS and user agent mismatch, and the TLS client itself. A person can change their face's surroundings far more easily than they can change how their traffic looks. ## What we flagged: the Flags tab Anything our checks raised on this person, with the reason attached. Empty means every run came through clean rather than nothing being looked at. ## Where they have been: the History tab Every confirmation this person appears in, newest first, including runs where they are only the evidence. A denial shows its reason inline, so `limit reached` tells you your own limit fired. This is the tab that gives you a baseline. One low-scoring run from a first-timer is thin. One low-scoring run from somebody with twelve clean ones behind them is a change, and changes are what you want to catch. ## Who they are to you: the summary panel **Claims** is the field to read first. It names the phone or email bound to this person, when it was proven and how. If there is one, this person can be recognised on any device, anywhere you run a check. If there is not, recognition stops at the device they used, and they will look new the next time they arrive on another one. **Attributes** repeats the estimates from the scan alongside how many verifications they have with you in total. **Activity** is their span with you: when they first and last verified, and how many devices we have seen them on. **Next:** [Exporting users](/docs/activity/exporting-the-list) # Reading Activity Source: https://docs.verifyyou.com/docs/activity/reading-activity The record of everyone who completed a check. **Activity** is the record of everyone who completed a verification, most recent first. [**Workspace Activity**](https://platform.verifyyou.com/dashboard/activity) covers every check you run. A row gives you the identifier they gave, or *No identifier given*, the time, a trust band, a trust score out of 100, the result, and a `TEST` badge if it came from test keys. The row menu opens that person's profile. **A verification's own Activity tab** adds the working controls: **Needs action**, **Export**, row selection, rows per page, and sending the check by email. Go here when you are working a list. Go to workspace Activity when you are looking somebody up. Email invites have their own funnel. On a verification running **Existing users**, the **Email invites** view tracks each invite through sent, opened, clicked and verified, so you can see who opened an invite and stopped, and who never opened it at all. Somebody who opens it and never finishes is telling you something different from somebody who never opened it. [Existing users setup](/docs/guides/existing-users) covers the sending itself. A verification's Activity tab, with the Filter and Export controls highlighted above the list of people ## Looking up individual users Search by email or phone. The support case: somebody says they cannot get in. The row tells you what actually happened, because a denial carries its reason. `limit reached` means your own verification limit fired and they have already passed once. A liveness failure means the camera never confirmed a live person, and trying again in better light might work. Two different answers to give them. The investigation case: you have seen something odd on your own platform from one account and you want to know what we saw. Search the identifier, open the person, and hold what we have against what you have, the location, the devices, and the history of every run they appear in. ## Filtering users The **Filter** opens 26 fields, and they fall into four groups worth knowing. **Where they came from**: Country, Region, City, Platform, Device type, Browser. Reach for these when you have a picture of where your real people are. If your panel is US-based and a cluster of sessions arrives from one city you have never recruited in, that is worth pulling out on its own, even before anything is flagged. **How they arrived**: VPN, Datacenter IP, Tor exit, Known abuser IP, TLS automation, TLS / UA mismatch, TLS client. Augie at Snorkel uses these to catch people who pass the check and still cannot hide their infrastructure: a cluster of accounts on datacenter IPs, when real people are working from laptops at home. **What the check saw**: Threat level, Face collision, Face collisions, Liveness denied, Liveness failure reason, Liveness attempts, Liveness confidence, Age low, Age high, Gender confidence. **Face collisions** is the one to start from on any audit. A collision is the same human passing a check twice, and [Configuration](/docs/verifications/configuration) decides what happens when one is found. **Housekeeping**: Environment separates test traffic from live, and **Conflicts with** pulls up the records that clash with a given one. **Next:** [Comprehensive trust score](/docs/activity/guest-profiles) # Start guide Source: https://docs.verifyyou.com/docs/get-started/getting-access How you get a workspace, and the four things to do in it. Our team creates your workspace and configures your first check with you. [Book a demo](https://sales.verifyyou.com). We work with you to understand your use case and get you set up to start seeing value immediately. We set it up around whatever you are gating, configure your first check, and email you an invite. From the **Team** page, once you are in. See [Team and roles](/docs/workspace/team-and-roles). Four things to do [once you are in](https://platform.verifyyou.com/dashboard). ## Try a HumanCheck Open the verification, go to **Integration**, and use **Run a test**. It runs the real check on test data with a test identity, so nothing touches your live records. Fail it on purpose too, so you have seen what your users see when it goes wrong. ## Understanding your use case Configuration allows you to adjust how the check behaves to match your use case. **What are you protecting?** Different use cases have different requirements, which we make simple to accommodate: A signup that allows one account per human refuses the second face outright. A userbase healthcheck runs over people who already have accounts, lets them all through, and tells you what it saw about each one. [Configuration](/docs/verifications/configuration) goes deeper on the concept and on which settings suit which job. Pick the preset that matches the job: **Signup Gate**, **Account Sweep**, **AI Data Labeling Fraud Audit** or **Build Your Own**. A preset is a sensible starting point for a general version of that job, not a finished setup. Two companies running signup gates often want different answers on how many times one person may pass, whether to ask for a phone or email, and what a duplicate should cost. Go through [Configuration](/docs/verifications/configuration) afterwards and make each setting match what you are actually protecting. See how common use cases work: [Gate a signup](/docs/use-cases/signup-gate), [Gate key actions](/docs/use-cases/gate-key-actions), [Userbase healthcheck](/docs/use-cases/userbase-fraud-audit), [Persistent verification](/docs/use-cases/persistent-verification). ## Start protecting your platform Choose how the check reaches people. No code gets you started with zero developer effort, so you can prove value early without interrupting your roadmap: share the verification's link, import your existing users and send each of them a request, or put one-time passwords in front of a survey. Code puts the check inside your own product with the SDK, with your server confirming the result. See [Integration](/docs/verifications/integration) for all four. ## Understanding your data **Activity** lists everyone who completed the check, newest first, and opening a row gives you everything we saw about that person. **Flags** collects the people our checks caught, with the reason. **Insights** rolls all of it up across a period, so you can see the shape of your traffic rather than one person at a time. See [Reading Activity](/docs/activity/reading-activity), [Flags](/docs/insights/flags) and [Reading Insights](/docs/insights/reading-insights). **Next:** [Creating a verification](/docs/verifications/creating-a-verification) # Glossary Source: https://docs.verifyyou.com/docs/glossary Every term this site uses, in plain language. Every product picks up its own vocabulary, and the rest of these docs read more easily once the words are pinned down. Here are the terms HumanCheck uses, in plain language, grouped by where you will meet them. Each definition stands on its own, and the links point to the pages that go deeper. These are the words the dashboard and the developer docs use, so what you read here lines up with what you will see on screen and in the API. ## The product ### HumanCheck The VerifyYou product: a short camera check that confirms a real, live, unique human, without ID documents. Companies place it in front of signups, surveys, and communities, anywhere that needs real people. [Guests and members](/docs/how-it-works/guests-and-members) has the full picture. ### Verification A configured check in your dashboard. Each verification carries its own configuration, branding, and integration, and has its own link, insights, and activity, so you can run one for signups and another for a survey, each set up its own way. [Creating a verification](/docs/verifications/creating-a-verification) walks the wizard that makes one. ### Session One run of a check by one person, from the moment it starts to a result or a walk-away. A session that reaches a decision is **settled**, and that is what Insights counts. [Guests and members](/docs/how-it-works/guests-and-members) follows one from start to finish. ### Redirect URL Where a person lands after they finish the check. Set on the verification, in the **Where people land** panel, and in redirect mode it is also where the confirmation token arrives, so set it before you go live. The [quickstart](/v3/dev/integrating/start-a-session) shows where it fits. ## Members and guests ### Member A person whose verified phone number or email is bound to their face record, which the developer docs call a **bound identity**. Ownership is proven with a one-time code during the flow, and that is what carries recognition across devices and browsers. [Guests and members](/docs/how-it-works/guests-and-members) goes deeper. ### Guest A person recognized by their device ID and nothing else: no phone, no email, no name. The developer docs call this **anonymous**. Recognition stays with that device, so a guest on a new device or a cleared browser looks like somebody new. [Guests and members](/docs/how-it-works/guests-and-members) has the comparison. ### Device ID The identifier that lets HumanCheck recognize a guest's device when it returns. A new device generally means a new device ID, which is why repeat visitors are better registered as members. [Guests and members](/docs/how-it-works/guests-and-members) covers the return moment. ### Skip check on known devices The setting that lets somebody returning on a device you already know through without repeating the scan. Off by default, otherwise a window you choose. A person who skips the scan still counts as a verification. [Configuration](/docs/verifications/configuration) covers the options. ## What the check proves ### Liveness The part of the check that confirms a real, present human is in front of the camera, not a photo, a replay, or a deepfake. [How it works](/docs/how-it-works/guests-and-members) walks through the whole flow. ### Uniqueness The part of the check that confirms this human has not already passed under a different identity. New checks are compared against the face vectors already held, which is how a first-timer is told apart from someone coming back under a new name. ### Face vector A numerical representation of the face's geometry, not a saved image. It is treated as sensitive biometric data, and verification images themselves are not stored. [Security and privacy](/docs/how-it-works/security-and-privacy) covers this honestly and in full. ## Matching and identity ### Collision A new check whose face matches a face record that already exists, meaning the same person is trying to pass again where your rules say they should not. Depending on your configuration, a collision is blocked outright or flagged as suspicious. ### Access type What the **Identity** panel calls its three options: *Linked by guest*, *Anonymous* and *Provided by you*. [Configuration](/docs/verifications/configuration) covers each. ### Group A parameter your integration sends when it starts a session, so one verification can gate several separate things and somebody may pass once in each. It is not a control on the Configuration tab. See [start a session](/v3/dev/integrating/start-a-session). ### Preset The starting point you pick in the wizard: **Signup Gate**, **Account Sweep**, **AI Data Labeling Fraud Audit** or **Build Your Own**. It sets the configuration, and you change what does not fit. ## Reading results ### Trust score A number out of 100 on a person. Everyone starts at 100, and every signal we collect while they move through the flow takes points off. A high score means few signals came back on them. It never blocks anyone on its own: the pass or fail is settled by your configuration. See [Threat levels and signals](/docs/insights/threat-levels-and-signals). ### Threat band How Insights groups scored sessions: **Low**, **Moderate**, **Elevated**, **High**. Higher means more concerning, which is the opposite direction to the trust score. The two describe one person from opposite ends. ### Threat families What led the score across scored passes: network, geography, accounts, automation and liveness. Which family dominates tells you whether one person is repeating themselves or many people are arriving the same way. Not the same as the five signal groups on a person, which are Capture, Person, Location, Device and Network. See [Comprehensive trust score](/docs/activity/guest-profiles). ### Settled A run that reached a decision. **Settled passes** on Insights counts them for the period you picked, alongside **liveness pass rate** and **elevated risk**. ### Needs action A filter on a verification's Activity tab that narrows the list to the sessions the product has singled out. It is not on workspace Activity. ### Environment The test and live split. Activity shows a `TEST` badge on test runs, and **Environment** is the filter that separates them. ### Decline A graceful exit from the check. Turning on the decline banner adds an opt-out to the modal and returns a declined event to your product, so a person who would rather not verify has a way out and you find out they took it. See [Branding your check](/docs/verifications/branding-your-check). ## Keys, tokens, and testing ### Test keys and live keys Your workspace has two secret keys, `sk_test_` for trying things out and `sk_live_` for production. Both stay on your server. Test and live are separate partitions and never mix, so a session minted with a test key confirms only with the test secret key. Publishable keys (`pk_`) are deprecated: start every session server side and open it in the browser. The [quickstart](/v3/dev/integrating/start-a-session) puts them to work. ### Simulator and test sessions Test keys run against a simulator, so the code path works end to end without involving a real person. There are no-code test sessions too, so you can rehearse before anything goes live. The [quickstart](/v3/dev/integrating/start-a-session) uses them. ### external\_id Your own identifier for the person being checked, passed when your server initializes a session with a secret key and echoed back on the confirmation. It lets you tie a result to your user without handing us their identity. [Account linking](/v3/advanced/account-linking) goes deeper. ### Confirmation token (vyt) The token that travels back with a person after a check, as the vyt query parameter in redirect mode or through the onComplete callback in embedded modes. Your server exchanges it for the authoritative result. Confirm each token once, and lock it afterwards if the flow should be one shot. The [quickstart](/v3/dev/integrating/start-a-session) shows the exchange in code. ### vyc A one-character hint that arrives next to the token: 1 suggests the check passed, 0 suggests it did not. It is only a hint for what to show on screen first. The token, confirmed from your server, is the truth. The [quickstart](/v3/dev/integrating/start-a-session) shows the two arriving together. ## Where to go next Run your first verification end to end, with or without code. Where these terms show up on screen, and how to read them. Set what the check asks for, how often one person may pass, and what happens on a duplicate. Look up the SDK and the API. # Alchemer setup Source: https://docs.verifyyou.com/docs/guides/alchemer Put HumanCheck in front of your Alchemer study. Five steps, no code. In Alchemer, exactly as you normally would, with one difference. 1. Build your study as usual. 2. Leave **page 1 empty**: your questions start on page 2. Page 1 is where the gate goes.

You now have: a normal Alchemer study with an empty first page.

In your [VerifyYou dashboard](https://platform.verifyyou.com/dashboard/verifications), open a verification and go to the **Integration** tab. 1. Make sure the method is **One-time passwords**. If it is set to something else, click **Switch method** and choose it. 2. Under **Your survey platform**, pick **Alchemer**. 3. Paste your **Alchemer survey link**, the share link from your study. 4. Set **how many one-time passwords** you need, one per expected respondent. You can always generate more later. 5. Click **Connect survey**. We generate your passwords and hand back your study link.

You now have: your study link (share this one, not your Alchemer link) and your one-time passwords. Click **Copy list** to copy them for the next step.

VerifyYou Integration tab, connect a survey platform VerifyYou study link ready with copy list
On **page 1** of your study. 1. Click **Add New: Action** and pick **Login/Password**. 2. Choose **"Individual (one-time) passwords that deactivate after use."** 3. Paste your password list, one per line, and save. Keep this action alone on page 1. Alchemer's own [Login/Password action](https://help.alchemer.com/help/login-password-action) enforces the gate before your first question shows.

You now have: a study that refuses anyone without a valid, unused password.

Alchemer one-time passwords editor
In the same Login/Password action. 1. Open the **Display** tab. 2. Set **Pre-populate Password** to the merge code `[url("vypw")]` and click **Save Action**.

You now have: a gate that opens itself for verified respondents. The password lands in the box on its own.

Alchemer pre-populate password setting
The link you hand out is the **study link from VerifyYou** (the `app.verifyyou.com/verification/...` one, from **Copy** in the dashboard), never your Alchemer link. This is the whole point: the VerifyYou link sends every respondent through the face check first, then redirects the verified ones into your Alchemer study with their one-time password already attached. Anyone who opens the raw Alchemer link skips the check, so only ever share the VerifyYou link, in your panel, your sample provider, or your invite email.

You now have: a fielded study where everyone who reaches your questions came through the check. That's the whole setup.

## What happens next
What your respondent sees See exactly what your respondent sees. The same mechanism, any platform Alchemer today. The same pattern works anywhere.
# Existing users setup Source: https://docs.verifyyou.com/docs/guides/existing-users Verify the people you already have, by email. Five steps, no code. In your [VerifyYou dashboard](https://platform.verifyyou.com/dashboard/verifications), if you already have a verification you want to use, skip to step 2. 1. Click **New verification** and pick your check. [Creating a verification](/docs/verifications/creating-a-verification) walks the wizard, and [Configuration](/docs/verifications/configuration) explains each setting. 2. Choose **No code** when the wizard asks how the check will reach people. 3. Add your logo and company name so the check carries your brand. [Branding your check](/docs/verifications/branding-your-check) shows what each part controls. 4. Continue through to **Integration**.

You now have: a live verification, branded as yours.

On the verification's **Integration** tab. 1. Click **Switch method** and choose **Existing users**. 2. Set where people land after they verify, under **Lands on**, with **Edit**. If you leave it unset, people see the default finish screen.

You now have: the import view, ready to take your list.

Switch method dialog with Existing users selected
In the **Add people** panel on the same tab. 1. Paste your list, or click **Upload CSV**. Whole spreadsheet rows are fine, with or without a header. The panel reads your list and tells you how many people are ready to add. 2. Click **Add**, and **Map columns** opens before anything comes in. Four fields sit at the top: **Email address**, then **IP address**, **Impact figure** and **Date for the figure**, each marked optional. We match your columns to them automatically, and each match is a dropdown, so if we guessed wrong you point it at the right column yourself. Only email is required. Leave the others unmapped and the import still runs. 3. Everything else in your file sits under **Extra data**, saved to each person's record, with a type on each column: currency, date, number, text. Tick a column under **Select columns to leave out** to skip it this time. Import whatever you would want to compare later. An IP address you hold comes back beside the one we see when that person verifies. 4. Check the count at the foot of the step, importing so many critical fields and so many extra columns, then click **Confirm mapping**. Rows with no email are skipped and counted, and empty cells stay empty rather than being filled in for you. Each person you add gets a verification link locked to their address. Adding people never sends anything, so you can build the list over several sittings and send once, when you are ready.

You now have: your people imported, each with their own locked link and your extra columns on their record, and nothing sent.

Under **Recently added**, click **View all** to open the verification's **Activity** tab. 1. Click the address shown in the header, next to **Send unsent**. The **Senders** panel opens. 2. Add your address with **Add sender** and confirm it. When it reads **Confirmed. Ready to send**, you can send. 3. To send from your own domain, publish the TXT and CNAME records shown under **Your own domain** with your DNS provider, then click **Re-check**. Each record flips to **Live** once it is seen. You can send before the records are live, and having them in place keeps your invites out of spam, so plan this in before a large batch.

You now have: a confirmed From address your invites will leave from.

On the **Activity** tab, with your people listed. 1. Select the people to invite, or use **Send unsent** to reach everyone you have not contacted yet. 2. Click **Send**. The dialog shows who is getting it and the address it leaves from. 3. Pick the message under **Message**: **Initial invite** for a first ask, or the re-verification message for people you are inviting back. **Edit** opens the copy so you can make it yours. 4. Confirm the send. We host the sending, so there is no mail system on your side to wire up. The **Email invites** view then tracks each invite through sent, opened, clicked and verified as people come through, so you can see who opened and stopped and who never opened at all, and [Reading Activity](/docs/activity/reading-activity) covers what to do with what arrives.

You now have: invites out, and results arriving on Activity as each person verifies. That's the whole setup.

**Beta note.** Existing users is in beta. Hosted sending also has a daily cap, and the counter on the Activity header shows where you stand against it today. ## What happens next
Reading Activity The record of everyone who completed a check. Userbase healthcheck The use case this method was built for.
# Google Forms setup Source: https://docs.verifyyou.com/docs/guides/google-forms Put HumanCheck in front of your Google Form. Five steps, one script you never edit. Google Forms cannot run a check before it shows a question. It has no hidden fields, no branching on a link parameter, and no way to ask an outside service anything while a respondent is looking at it. So HumanCheck holds the line in two places instead of one. The check happens on our side, before anyone reaches your form. The clean-up happens in your own spreadsheet, the moment somebody submits. Together they mean only verified people reach your questions, and anything that slips past by going straight to your form URL is removed from your results within seconds. Somebody who skipped the check can still open your form and press submit. What they cannot do is leave a trace in your data or get paid for it. If you need unverified people to be unable to load the page at all, [Qualtrics](/docs/guides/qualtrics) and [Alchemer](/docs/guides/alchemer) can do that and Google Forms cannot. In Google Forms, exactly as you normally would, with two additions. 1. Add a **Short answer** question called **Verification code**. Mark it **Required** and put it on the first page. Capitals and spacing do not matter. 2. Underneath it, write something like "Filled in automatically, please leave this alone." 3. Open the **Responses** tab and click the Sheets icon to send your responses to a Google Sheet. The rest of the setup lives in that sheet. 4. In **Settings**, open **Responses** and turn **off** "Allow response editing." Without this, somebody could submit and then edit a valid code in afterwards.

You now have: a normal Google Form with a code box on page one, and a spreadsheet collecting the answers.

In your [VerifyYou dashboard](https://platform.verifyyou.com/dashboard/verifications), open a verification and go to the **Integration** tab. 1. Make sure the method is **One-time passwords**. If it is set to something else, click **Switch method** and choose it. 2. Set **how many one-time passwords** you need. Ask for at least twenty percent more than the respondents you expect. Passwords cost nothing, and running out mid-study screens out real people. 3. Click **Copy list** to copy your passwords. You need them in step four.

You now have: your study link, which is the one you share, and your one-time passwords.

Open the Google Sheet your responses go to, not the form. 1. Open **Extensions**, then **Apps Script**. 2. Delete anything already in the editor and paste in the script from the bottom of this page. 3. Click **Save**. 4. Click **Deploy**, then **New deployment**. Click the gear beside "Select type" and choose **Web app**. 5. Set **Execute as: Me** and **Who has access: Anyone**, then click **Deploy** and approve the permissions. 6. In the function dropdown choose **`setUp`** and click **Run**. When Google warns that it has not verified this app, click **Advanced**, then **Go to (your project)**, then **Allow**. That message appears because this is a private script you own and have not published to anyone else. Your spreadsheet now has a tab called **VerifyYou codes**. Cell **G5** holds your **access link**, which you need in step five.

You now have: a spreadsheet that checks every submission, and an access link that fills the code box on its own.

On the **VerifyYou codes** tab. 1. Paste your passwords into **column A**, starting at A2. 2. Type `Unused` beside each one in **column B**. That tab is your ledger. Status moves from Unused, to Issued when we hand somebody that password, to Used when they actually submit. It is also how you check the gate is running: if people are completing your survey and nothing is moving to Used, something is wrong. Keep this spreadsheet private. Anyone who can read your passwords can use them.

You now have: a study that accepts a response only when it arrives with a valid, unused password.

Two links to keep straight. In VerifyYou, set the destination for verified people to the **access link from cell G5**. Then the link you hand out to respondents is the **study link from VerifyYou**, never your Google Form link. Anyone who opens the raw form link skips the check, so only ever share the VerifyYou link, in your panel, your sample provider, or your invite email.

You now have: a fielded study where everyone who reaches your questions came through the check. That's the whole setup.

Your respondent goes straight from the check into your survey with nothing to click. The survey runs inside the access link's own page, so the address bar keeps saying `script.google.com` while they answer. Underneath the form there is a small "Survey not loading? Open it in a new page" line, for the rare case where a form will not display nested. ## Test it before you field The script starts in a safe mode where nothing is deleted. It writes what it would have done into a column called **VerifyYou status** in your responses sheet. Stay in that mode until all six behave. Open your access link with `?vyt=test123` on the end. You should go straight into your survey with the code already filled in, without clicking anything. One password should now say Issued. Submit, and the status column should say `verified` while that password flips to Used. Open the exact same link again. You should get the **same** password back, not a new one. This is what stops somebody who abandons your survey and returns from consuming two. Open your plain form URL, leave the code box empty, and submit. The status should say `REJECTED: no code`. Type any invented string into the code box and submit. The status should say `REJECTED: code not in pool`. Submit once from a prefilled link, then open that same link again and submit a second time. The first should say `verified`. The second should say `REJECTED: code already used`. This is the check that proves one password cannot buy two responses, which is the whole reason the pool exists. Rename the VerifyYou codes tab, submit anything, then rename it back. The status should say `unchecked`, and never `verified`. This one matters most: it proves that if your setup breaks, it tells you rather than quietly waving everybody through. When all six behave, change `onInvalid` near the top of the script from `'FLAG'` to `'DELETE'` and click **Save**. That takes effect straight away, because the check that runs on each submission always uses your latest saved code. Deploying is a separate thing, and it is the step people lose an hour to. Anything that changes how your **access link** behaves goes live only when you click **Deploy**, then **Manage deployments**, then the pencil icon, then set **Version** to **New version**, then **Deploy**. Save the file without doing that and you will still be served the old version, and conclude the change did not work. ## When something looks wrong The script is not running. Run `setUp` again from the script editor. Your codes tab is named something other than `VerifyYou codes`, or it is empty. Check the tab name matches exactly. The passwords in your sheet are not the ones being handed out. Copy your list again and paste it in fresh. It depends what you changed. `onInvalid` takes effect the moment you save, because the submission check always runs your latest saved code. Anything that changes how your access link behaves needs a new deployed version: **Deploy**, **Manage deployments**, the pencil icon, then **Version: New version**. Your form is not displaying nested inside the access link. They can click that line to carry on, and you should tell us, because it means we should move you to the button version instead. The access link was opened without a token on the end. That is expected if you opened it directly rather than through your VerifyYou study link. If real respondents see it, the token is not reaching the script, which is a VerifyYou setting rather than a problem with your form. Your organisation's Google administrator has switched off public web apps. You will need their help, or an account without that restriction. Run `vyPoolStatus` in the script editor any time. It reports how many are unused, how many we issued that nobody submitted, and how many people spent. ## Your data stays yours Your passwords and your responses live in your Google account. What we hold is a list of verification sessions with no survey answers attached to any of them. There is no point at which we have both halves, so we could not connect a person to their answers even if we were asked to. If your ethics board asks how the two are kept apart, that is the answer, and you are welcome to point them at this page. The script makes no connections outside your Google account. ## The script Paste this whole thing into **Extensions**, then **Apps Script**, on your responses spreadsheet. The only line you ever change is `onInvalid`, and only when you finish testing. ```javascript HumanCheck backstop wrap theme={null} /** * VerifyYou HumanCheck backstop for Google Forms * Version 1.0 * * WHAT THIS DOES * Every time someone submits your form, this checks the verification code they * arrived with against your pool of issued codes. Valid and unused means the * response is kept and the code is marked used. Missing, unknown, or already * used means the response is removed, from both your spreadsheet and the form. * * It makes no external network calls. Everything happens inside your own Google * account, so nobody outside it sees your codes or your response data. * * SETUP * Run setUp() once from this editor and approve the permissions prompt. * * This script is bound to the RESPONSE SPREADSHEET, not the form. That is * deliberate: the spreadsheet's form-submit trigger hands us the exact row that * was just written, so there is no guessing and no race with the row appearing. */ const VY = { // Tab holding your codes. // A: Code B: Status (Unused/Issued/Used) C: Issued at D: Used at E: Token // G2: link template G5: dispenser URL (once deployed) poolTabName: 'VerifyYou codes', // Exact title of the short answer question that receives the code. codeQuestionTitle: 'Verification code', // Optional. If you add a short answer question with this title, the participant // ID from your recruitment platform is carried into it too. Leave the question // out of your form if you do not need it. participantQuestionTitle: 'Participant ID', // 'DELETE' removes bad responses. // 'FLAG' keeps everything and writes a status column instead. Use FLAG while // you are testing so you can see what would have happened before it is final. onInvalid: 'FLAG', // Header written into the response sheet when onInvalid is 'FLAG'. statusColumnHeader: 'VerifyYou status', // How the dispenser hands the respondent over to the form. // // A silent redirect of the whole browser tab is NOT possible here, and that is // a Google restriction rather than something fixable in this file. Apps Script // serves web apps inside a restricted iframe that is not granted // allow-top-navigation, and grants allow-top-navigation-by-user-activation // only to stand-alone script projects, not container-bound ones like this. // Google's own documented advice is to use a link or button instead. // // 'BUTTON' = one clean "Start the survey" button. Honest and obvious, but it // is a click, on a script.google.com page, right after you asked // someone for a face scan. // 'EMBED' = no click. Loads the form inside the page instead of navigating // away, which sidesteps top navigation entirely. The address bar // keeps saying script.google.com and the form runs nested. dispenserMode: 'EMBED', }; // --------------------------------------------------------------------------- // SETUP // --------------------------------------------------------------------------- /** * Run this once. Creates the code pool tab if it is missing and installs the * form-submit trigger. Safe to run again; it will not create duplicate triggers. */ function setUp() { const ss = SpreadsheetApp.getActiveSpreadsheet(); // doGet runs outside the spreadsheet's context, where "active spreadsheet" is // not reliably defined. Stash the ID now and open by ID later. PropertiesService.getScriptProperties().setProperty('VY_SS_ID', ss.getId()); let pool = ss.getSheetByName(VY.poolTabName); if (!pool) pool = ss.insertSheet(VY.poolTabName); pool.getRange('A1:E1').setValues([['Code', 'Status', 'Issued at', 'Used at', 'Token']]); pool.setFrozenRows(1); // Remove any trigger we installed previously, so re-running is safe. ScriptApp.getProjectTriggers() .filter(function (t) { return t.getHandlerFunction() === 'vyOnFormSubmit'; }) .forEach(function (t) { ScriptApp.deleteTrigger(t); }); ScriptApp.newTrigger('vyOnFormSubmit') .forSpreadsheet(ss) .onFormSubmit() .create(); const template = vyWritePrefillTemplate_(ss, pool); const codes = vyCountCodes_(pool); const message = 'VerifyYou backstop installed.\n\n' + 'Codes: ' + codes.total + ' total, ' + codes.unused + ' unused, ' + codes.issued + ' issued, ' + codes.used + ' used\n' + 'Dispenser: ' + (vyDispenserUrl_() || 'not deployed (optional, see runbook)') + '\n' + 'Mode: ' + VY.onInvalid + '\n' + 'Link template: ' + (template ? 'ready' : 'PROBLEM, reason is in G2') + ' (cell G2 of the "' + VY.poolTabName + '" tab)\n\n' + (codes.total === 0 ? 'Next step: paste your codes into column A of the "' + VY.poolTabName + '" tab.' : 'You are ready to test.'); Logger.log(message); if (template) Logger.log(template); try { ss.toast(message, 'VerifyYou', 15); } catch (err) { /* toast is best effort */ } } /** * Asks Google to generate the pre-fill link for us, rather than anyone building * it by hand. * * Hand-assembling this URL has three silent failure modes: appending &entry to a * URL with no query string, using the editor URL instead of the public one, and * using a forms.gle short link, which drops the query string on redirect. Each * one loads the form normally with an empty code field, so it fails invisibly. * toPrefilledUrl() sidesteps all of them because Google emits the URL itself: * correct public ID, correct entry IDs, correct encoding. * * The response is created but never submitted, so nothing lands in your results. */ function vyGetPrefillTemplate() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const pool = ss.getSheetByName(VY.poolTabName); const url = vyWritePrefillTemplate_(ss, pool); if (url) Logger.log(url); return url; } /** * Titles are compared trimmed and lower-cased everywhere. "Verification Code" * and "Verification code" are the same question as far as this script is * concerned. Exact matching looks tidy and is a trap: Google Forms gives no hint * that capitals matter, so the natural way to type a title silently breaks * everything downstream. */ function vyNorm_(s) { return String(s || '').trim().toLowerCase(); } function vyWritePrefillTemplate_(ss, pool) { const formUrl = ss.getFormUrl(); if (!formUrl) { return vyTemplateProblem_(pool, 'This spreadsheet is not linked to a form. Open the form, go to the Responses tab, and link it here.'); } const form = FormApp.openByUrl(formUrl); let response = form.createResponse(); let found = false; const seen = []; form.getItems().forEach(function (item) { const type = item.getType(); const isText = type === FormApp.ItemType.TEXT; const isPara = type === FormApp.ItemType.PARAGRAPH_TEXT; if (!isText && !isPara) return; const title = item.getTitle(); seen.push(title); const answer = function (value) { return isText ? item.asTextItem().createResponse(value) : item.asParagraphTextItem().createResponse(value); }; // The code question, and optionally a participant ID question, get // placeholders our gate substitutes at redirect time. if (vyNorm_(title) === vyNorm_(VY.codeQuestionTitle)) { response = response.withItemResponse(answer('__VY_CODE__')); found = true; } else if (vyNorm_(title) === vyNorm_(VY.participantQuestionTitle)) { response = response.withItemResponse(answer('__VY_PID__')); } }); if (!found) { return vyTemplateProblem_(pool, 'No question matching "' + VY.codeQuestionTitle + '" was found. ' + 'Text questions in your form: ' + (seen.length ? seen.join(' | ') : 'none') + '. ' + 'Either rename one of them, or change codeQuestionTitle at the top of this script.'); } const url = response.toPrefilledUrl(); if (pool) { pool.getRange('G1').setValue('Link template'); pool.getRange('G2').setValue(url); pool.getRange('G4').setValue('Access link (paste THIS into VerifyYou)'); pool.getRange('G5').setValue(vyDispenserUrl_() || 'not deployed yet, see step 6 of the runbook'); pool.setColumnWidth(7, 460); } return url; } /** * Writes the reason into G2 rather than leaving it blank. A blank cell tells you * nothing; the reason tells you everything, and it appears where you are already * looking instead of in an execution log you would have to go and open. */ function vyTemplateProblem_(pool, message) { Logger.log(message); if (pool) { pool.getRange('G1').setValue('Link template (PROBLEM)'); pool.getRange('G2').setValue(message); pool.setColumnWidth(7, 460); } return ''; } /** The deployed web app URL, or '' if this has not been deployed yet. */ function vyDispenserUrl_() { try { return ScriptApp.getService().getUrl() || ''; } catch (err) { return ''; } } // --------------------------------------------------------------------------- // THE DISPENSER (web app) // --------------------------------------------------------------------------- // // TEST HARNESS, not the shipping design. Read this before relying on it. // // Deploy → New deployment → Web app, "Execute as: Me", "Who has access: Anyone". // Google hosts it. Paste the resulting URL into VerifyYou as the verified // destination, INSTEAD of your form URL. // // Three things to know: // 1. It cannot verify the token. Confirming a token server-side needs the // VerifyYou SECRET key, which must never sit in a researcher's script. So // this checks that a token is present and refuses to issue twice for the // same one. That stops sharing and refreshes. It does not stop somebody // deliberately sending made-up tokens to drain the pool. // 2. It fails CLOSED. If this breaks, hits a quota, or loses authorization, // verified people get no code and cannot enter the study at all. The // submit-side check fails open; this one is the opposite, and it sits on // the critical path of live fielding. // 3. "Anyone, even anonymous" can be disabled by a Workspace administrator, // and on some account types it is not offered. If you cannot select it, // this approach is not available on that account. function doGet(e) { const params = (e && e.parameter) || {}; // Two ways a password can arrive. // // PASSTHROUGH (preferred): VerifyYou already issues a one-time password per // verified human and drops it on the link, which is how the Alchemer setup // works, where it lands as ?vypw=. Nothing left to decide, so just carry it // into the form. VerifyYou owns issuance, reissue on abandonment, and running // out, which is exactly where that belongs. // // CLAIM (fallback): only a ?vyt= token arrives, so this script picks the next // unused password out of the sheet itself. const supplied = String(params.vypw || params.vyp || '').trim(); const token = String(params.vyt || params.vyc || '').trim(); const pid = String(params.pid || params.PROLIFIC_PID || params.participant || '').trim(); if (!supplied && !token) { return vyDispenserPage_('This link needs to be opened through your study link, not directly.'); } const lock = LockService.getScriptLock(); try { lock.waitLock(30000); } catch (err) { return vyDispenserPage_('Busy right now. Please refresh in a few seconds.'); } try { const ssId = PropertiesService.getScriptProperties().getProperty('VY_SS_ID'); if (!ssId) return vyDispenserPage_('Not set up yet. Run setUp from the script editor.'); const ss = SpreadsheetApp.openById(ssId); const pool = ss.getSheetByName(VY.poolTabName); if (!pool) return vyDispenserPage_('No codes tab found. Run setUp from the script editor.'); const template = String(pool.getRange('G2').getValue() || ''); if (template.indexOf('__VY_CODE__') === -1) { return vyDispenserPage_('No link template. Run setUp from the script editor.'); } // Passthrough beats claiming. If VerifyYou already picked the password, use // it and note it against the pool row so the sheet still shows what happened. let code = supplied; if (code) { vyMarkIssued_(pool, code, token); } else { code = vyClaimCode_(pool, token); } if (!code) { // Pool exhausted. Verified people arriving with nothing is the worst // failure this design has, because in the results they look like bots. return vyDispenserPage_( 'This study has run out of access codes. Please contact the researcher; ' + 'you have not done anything wrong.'); } let url = template.split('__VY_CODE__').join(encodeURIComponent(code)); url = url.split('__VY_PID__').join(encodeURIComponent(pid)); return vyRedirect_(url, String(params.debug || '') === '1'); } catch (err) { Logger.log('VerifyYou dispenser error: ' + err); return vyDispenserPage_('Something went wrong opening the survey. Please contact the researcher.'); } finally { lock.releaseLock(); } } /** * Claims a code for this token. Idempotent by token: the same person refreshing, * or coming back after abandoning, gets the SAME code rather than spending * another one. Returns '' when the pool is exhausted. */ function vyClaimCode_(pool, token) { const last = vyLastCodeRow_(pool); if (last < 2) return ''; const rows = pool.getRange(2, 1, last - 1, 5).getValues(); // Already issued to this token? Hand back the same code. for (let i = 0; i < rows.length; i++) { if (String(rows[i][4] || '').trim() === token) return String(rows[i][0]).trim(); } // Otherwise take the first Unused one. for (let i = 0; i < rows.length; i++) { const code = String(rows[i][0] || '').trim(); if (!code) continue; if (String(rows[i][1] || '').trim().toLowerCase() !== 'unused') continue; pool.getRange(i + 2, 2, 1, 2).setValues([['Issued', new Date()]]); pool.getRange(i + 2, 5).setValue(token); SpreadsheetApp.flush(); return code; } return ''; } /** * Marks a password VerifyYou already chose as Issued, so the sheet reflects * reality. Deliberately quiet if the password is not in the pool: VerifyYou is * the authority on what it issued, and the submit-side check is what decides * whether a response is kept. */ function vyMarkIssued_(pool, code, token) { const last = vyLastCodeRow_(pool); if (last < 2) return; const rows = pool.getRange(2, 1, last - 1, 2).getValues(); for (let i = 0; i < rows.length; i++) { if (String(rows[i][0]).trim() !== code) continue; if (String(rows[i][1] || '').trim().toLowerCase() === 'unused') { pool.getRange(i + 2, 2, 1, 2).setValues([['Issued', new Date()]]); if (token) pool.getRange(i + 2, 5).setValue(token); SpreadsheetApp.flush(); } return; } } /** * Hands the respondent over to the form. See VY.dispenserMode for why a silent * whole-tab redirect is not on the menu. */ function vyRedirect_(url, debug) { const html = (VY.dispenserMode === 'EMBED' && !debug) ? vyEmbedHtml_(url) : vyButtonHtml_(url, debug); return HtmlService.createHtmlOutput(html) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .setTitle('Your survey'); } /** * One button. It still attempts the automatic hop first, which costs nothing and * covers the case where a browser or a future deployment type does permit it. * Anchors carrying target="_top" are user-initiated, which is the one form of * top navigation those restrictions do allow. */ function vyButtonHtml_(url, debug) { const attr = String(url).replace(/&/g, '&').replace(/"/g, '"').replace(/' + '