Skip to main content

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.

Supersedes identity mapping

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, a name, 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's identifier (the account's unique id within that store), a display name, and — when the link was created from directory data — the directory's userType (Member or Guest for 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.
identity linking diagramidentity linking diagram

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 attributeJWT payload claimWhere it lives on the VO identity
Tenant IDtidthe identity store's identifier
User Object IDoididentityStoreLinks[].identifier
User display namenameidentityStoreLinks[].name (and identity name)
tip

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.

saveIdentity: upsert an identity and its store link
mutation SaveIdentity($input: IdentityInput!) {
saveIdentity(input: $input) {
id
name
primaryIdentityStoreLink {
identityStoreId
identifier
}
}
}
Save an identity from a signed-in user's JWT claims
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:

Read an identity with all of its linked accounts
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:

Manual identity input
{
"issuer": "manual",
"identifier": "citizen@outlook.com",
"name": "Mary Citizen"
}
tip

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.

Consolidated issuances across a canonical identity and its merged children
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:

Query an identity by ID with its presentations
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 fieldUse instead
issuerprimaryIdentityStoreLink.identityStore.identifier or identityStoreLinks
issuerLabelidentityStoreLinks[].identityStore (its name / configured label)
identifierprimaryIdentityStoreLink.identifier or identityStoreLinks
identityStoreIdprimaryIdentityStoreLink.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.