Managing People

Enrolment invitations, subjects, credentials, identification and the authentication log.

Everything that is not the sign-in itself: inviting somebody to enrol, finding them again, revoking a passkey, resolving a face to a subject at a desk, and reading what happened.

Enrolling somebody

One call. It creates the subject if they do not exist yet and returns the link to send them.

const invite = await gid.enrollments.invite({
  externalId: "emp-1",
  email: "ana@acme.com",
  consentText: "Acme will process an irreversible representation of your face so you can sign in without a password.",
  policyVersion: "acme-2026-01",
  lawfulBasis: "art9_2_a",
  returnTo: "https://app.acme.com/enrolled",   // a registered origin
});

await db.save({ externalId: "emp-1", sub: invite.sub });

sendEmail("ana@acme.com", invite.url);

Store the sub. It is in every ID token and every security event, and it is the only thing linking either back to your own record. An event names a sub and nothing else — no email, no external id — and you cannot recover it from a face later.

consentText, policyVersion and lawfulBasis are required and have no defaults — the provider records the exact sentence the person was shown, and the policy version it came from.

When the page sends them back to returnTo, check once:

await gid.enrollments.status("emp-1");   // "submitted" or "active" when done

Do not poll. A stream subscribed to credential-change is told when the template becomes usable — see Receiving events.

Finding somebody

await gid.subjects.get("emp-1");         // by your own identifier
await gid.subjects.getBySub(sub);        // by the one in a token or an event

getBySub is the one you need in an event handler, where a sub is all you are given.

Listing is paged, and iteration handles the cursor for you:

for await (const subject of gid.subjects.iterate({ status: "active" })) {
  …
}

Ending access

await gid.subjects.suspend("emp-1");       // reversible
await gid.subjects.deactivate("emp-1");    // they are gone from your org
await gid.subjects.erase("emp-1");         // the template is destroyed

These are three different things and the difference matters. suspend is a state you can lift. erase destroys the biometric material and cannot be undone — a person erased must enrol again from scratch, because there is nothing left to compare against.

Passkeys are separate from the face:

await gid.credentials.list("emp-1");
await gid.credentials.revoke("emp-1", credentialId);

Never key material — a credential row says a passkey exists and when it was used, not what it is.

Identifying somebody at a desk

A hosted 1:N lookup, for a reception desk or a kiosk where nobody has typed an identifier.

const session = await gid.identifications.start({
  returnTo: "https://app.acme.com/desk",
});
// send the operator to session.url

const result = await gid.identifications.get(session.id);

It issues no token and signs nobody in. The answer is a sub or nothing — no candidate list, no score, no count, and no distinction between "nobody matched" and "nobody is enrolled".

Reading the log

const page = await gid.audit.authentications({
  externalId: "emp-1",
  since: "2026-10-01T00:00:00Z",
  decision: "denied",
});

No score and no threshold — not in the rows, and not as a filter.