Identity linking
This guide describes how VO identities link back to your organisation's own identities (internal users, partners, and customers), so issuances and presentations can be reliably resolved to the correct person.
A VO identity is a minimal person record. It can be linked to one or more accounts from your identity stores, and two identities that turn out to be the same person can be merged so their credentials and activity are seen together.
For details on setting up identity stores, see the Identity Store documentation.
This article replaces the earlier "identity mapping" model, where an identity carried a single issuer / identifier pair. Those fields are now deprecated (see Deprecated fields) in favour of identity-store links. The issuer / identifier upsert flow via saveIdentity still works for backward compatibility.
Concepts
- Identity — a VO-native person record. It has an
id, aname, and zero or more identity-store links. It holds no personal data beyond the name. - Identity store link (
IdentityStoreLink) — a link between an identity and an account in one of your identity stores. Each link carries the store'sidentifier(the account's unique id within that store), a displayname, and — when the link was created from directory data — the directory'suserType(MemberorGuestfor Entra; null means unknown, not member). An identity can link accounts from several stores. - Primary link (
primaryIdentityStoreLink) — the link an identity is displayed and resolved by when a single account must be chosen. - Merged identity — when the same person exists as two identities, one is merged into the other. The surviving identity is the canonical identity; the merged identity is retained for history but new activity resolves to the canonical.
How an account maps to a link
Each identity store provides a unique store identifier, and each account within it a unique account identifier. For an Entra ID store, these are the tenant tid and user oid claims from the JWT:
| Entra ID user attribute | JWT payload claim | Where it lives on the VO identity |
|---|---|---|
| Tenant ID | tid | the identity store's identifier |
| User Object ID | oid | identityStoreLinks[].identifier |
| User display name | name | identityStoreLinks[].name (and identity name) |
Custom labels can be configured for each identity store so the Composer displays a human-readable label instead of the raw store identifier. See the instance configuration guide.
Creating and maintaining identities
Upsert by issuer and identifier (backward compatible)
saveIdentity creates or updates an identity from an issuer (store identifier) and identifier (account identifier). If no identity store exists for the issuer, one is created automatically with an inferred type (Entra or Manual), and the account is linked.
mutation SaveIdentity($input: IdentityInput!) {
saveIdentity(input: $input) {
id
name
primaryIdentityStoreLink {
identityStoreId
identifier
}
}
}
function saveIdentity(user: JwtPayload) {
return client.mutate({
mutation: saveIdentityMutation,
variables: { issuer: user.tid, identifier: user.oid, name: user.name },
})
}
You can also include the identity object directly on createIssuanceRequest to upsert and link the identity in the same operation as the issuance.
Create and manage identities explicitly
For linking multiple accounts to one person, use the explicit mutations:
createIdentity(name)— create a person record with a name only, no store link yet.updateIdentityName(identityId, name)— rename an identity.linkIdentityStoreToIdentity(identityId, identityStoreId, identifier, name)— link an account from an identity store to the identity.unlinkIdentityStoreFromIdentity(identityId, linkId)— remove a link.setPrimaryIdentityStore(identityId, linkId)— choose which link is primary.
query IdentityLinks($identityId: ID!) {
identity(id: $identityId) {
id
name
primaryIdentityStoreLink {
id
identifier
}
identityStoreLinks {
id
identifier
name
isPrimary
identityStore {
id
name
}
}
}
}
Identities not backed by an identity store
For people who are not in one of your identity stores, use manual as the issuer. We recommend an email address as the identifier:
{
"issuer": "manual",
"identifier": "citizen@outlook.com",
"name": "Mary Citizen"
}
The Composer labels identities whose issuer is manual as "Manually Issued".
Merging identities
The same person can end up as two identities — for example, an account in two different stores, or a manual identity that is later matched to a store account. Merging brings them together.
mergeIdentity(sourceIdentityId, targetIdentityId)— merges the source into the target. The source's identity-store links move to the target, the source is marked as merged, and the target (the canonical identity) is returned. The source's credentials remain valid and presentable — the merge does not reissue or move them.unmergeIdentity(identityId)— clears the merged marker, making the identity standalone again. The identity-store links that moved during the merge stay with the canonical identity, so those accounts continue to resolve to it; re-link an account to the unmerged identity if it should resolve there instead.
A merged identity exposes its relationship on the Identity type:
isMerged— whether this identity has been merged into another.canonicalIdentity— the surviving identity that new activity resolves to.mergedIdentities— on a canonical identity, the identities merged into it.
Consolidated view of a canonical identity
A canonical identity's data can be viewed consolidated with its merged children's. Pass includeMerged: true on the identity-scoped queries — issuances, presentations, wallets, passkeys, temporary access passes and async issuances all accept it — to return the whole family, and IdentityWhere.includeMerged to include merged identities in findIdentities.
query ConsolidatedIssuances($identityId: ID!) {
findIssuances(where: { identityId: $identityId, includeMerged: true }) {
id
identity {
id
name
}
}
}
includeMerged defaults to false, so existing single-identity queries are unchanged.
Behaviour to be aware of
- Sign-in resolves to the canonical. When a holder signs in with a credential issued to an identity that has since been merged, the session resolves to the canonical identity — so activity accrues against the surviving record.
- Revocation is a canonical-level action. Revoking issued credentials at the identity level (
revokeIdentityIssuances) revokes the whole merged family — the canonical identity plus every identity merged into it. Calling it against a merged identity is rejected; target the canonical identity instead. - Merge and unmerge are logged. Each merge and unmerge is recorded in the identity audit trail (who and when) and emitted to the application logs, for traceability when troubleshooting a person's identity history.
Querying identity data
Identities can be queried one or many, by VO identity id or by your organisation's own issuer/identifier key:
identity— one identity by ID.identities— many identities by ID.identityByIdentifier— one identity by issuer and identifier.identitiesByIdentifiers— many identities by issuer and identifier.findIdentities— identities by name/issuer, withincludeMergedto surface merged identities.
query IdentityWithPresentations($identityId: ID!) {
identity(id: $identityId) {
id
name
primaryIdentityStoreLink {
identifier
}
presentations {
presentedAt
presentedCredentials {
issuer
type
faceCheck {
matchConfidenceScore
}
}
requestedBy {
id
name
isApp
}
}
}
}
Using identities in the Composer
Once identities exist:
- They act as an API entry point to find related data (issuances, remote issuances, presentations, wallets).
- The Composer supports searching for activity by linked identity.
- Admin users can find, list and filter identities via the quick search or the Issuees page, where a canonical identity's page shows its linked accounts, its merged identities, and its consolidated credentials and presentations.
Deprecated fields
The following Identity fields are deprecated. They still resolve (populated from the primary link) for backward compatibility, but new integrations should read from identityStoreLinks / primaryIdentityStoreLink instead:
| Deprecated field | Use instead |
|---|---|
issuer | primaryIdentityStoreLink.identityStore.identifier or identityStoreLinks |
issuerLabel | identityStoreLinks[].identityStore (its name / configured label) |
identifier | primaryIdentityStoreLink.identifier or identityStoreLinks |
identityStoreId | primaryIdentityStoreLink.identityStoreId or identityStoreLinks |
The issuer and identifier fields on IdentityInput (used by saveIdentity) are not deprecated — that upsert flow remains supported and lazily creates the identity store and link.