Skip to content

Identity Merges

When one person ends up with two accounts, a merge folds the duplicate into the survivor. Approving one does three things at once:

  • every moved alias is reassigned to the target user,
  • a redirect is written from the source user id to the target, and
  • the merge record is stamped completed with the ids it actually moved.

Anyone may request a merge; only a platform admin may execute one.

Requesting

snug alias merge request --source docs4-dup-acct --target docs4-keep-acct \
  --reason "Duplicate account created during signup"
{
  "merge_id": "0199e100-0000-7000-8000-000000000002",
  "source_user_id": "docs4-dup-acct",
  "target_user_id": "docs4-keep-acct",
  "status": "pending_review",
  "alias_ids_to_move": [],
  "moved_alias_ids": [],
  "created_at": "2026-08-28T23:10:22.272350Z",
  "completed_at": null
}

Leaving alias_ids_to_move empty means all of the source's active aliases. Pass --alias-id (repeatable) to move a subset. Source and target must differ, or the request is 400 invalid_request. Nothing checks that either id belongs to a real user, so a typo in --target produces a merge that completes into an id nobody owns.

Approving and rejecting

snug alias merge approve --merge-id 0199e100-0000-7000-8000-000000000002
snug alias merge reject  --merge-id mrg_xTgqqZNGQXDHRvSjWjJC --reason "evidence insufficient"

Each acts on a merge still in pending_review, and both need a platform admin token - an ordinary caller gets 403 insufficient_permissions. Approval reports what actually moved:

{
  "merge_id": "0199e100-0000-7000-8000-000000000002",
  "source_user_id": "docs4-dup-acct",
  "target_user_id": "docs4-keep-acct",
  "status": "completed",
  "moved_alias_ids": [
    "als_DaNVqFLgDSAxAFubRccq",
    "als_xTmBbhusDKTdzQnKXeJy"
  ],
  "completed_at": "2026-08-28T23:10:27.167674Z"
}

There is no endpoint that reads a merge back. Re-approving a completed merge (or re-rejecting a rejected one) is a no-op returning the current record, which is the closest thing to a read. Crossing between the two terminal states is refused with 409 invalid_merge_transition.

What the redirect does

The redirect is what makes a merge stick. Afterwards the source user id still works everywhere: resolving any moved alias returns the target user, and snug alias list --user docs4-dup-acct looks the target up instead. Listing still applies the caller's own visibility, so an outsider sees an empty page where an admin sees both aliases.

Redirects are collapsed, not chained. Merging dup -> keep and then keep -> final rewrites the first pointer so dup lands on final directly:

docs4-alias-dup   -> docs4-alias-final
docs4-alias-dup2  -> docs4-alias-final
docs4-alias-keep  -> docs4-alias-final

The redirect is written whether or not you moved everything. A partial merge leaves the unmoved aliases owned by the source on paper while resolution already points at the target, so an alias can report user_id: docs4-alias-dup2 on GET and still resolve to docs4-alias-final. Move everything unless you have a reason not to.

Conflict policy

conflict_policy.revoked_aliases decides what happens to the source's revoked aliases, and only bites when you name them in alias_ids_to_move - an empty list sweeps up active aliases only, so revoked ones stay put either way. With a source holding one revoked and one active alias, both named explicitly:

  • preserve_revoked (default) moved both, leaving the revoked one attached to the survivor with its revoked_at intact.
  • drop_revoked moved only the active one.
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source_user_id":"docs4-alias-dup3","target_user_id":"docs4-alias-final",
       "reason":"Drop the dead link",
       "conflict_policy":{"revoked_aliases":"drop_revoked"}}' \
  http://localhost:4000/api/v1/alias/merge

The body also accepts conflict_policy.primary_identity and conflict_policy.metadata. Both are stored on the merge record but are not consulted when it executes, so do not rely on them to pick a winning primary or reconcile metadata - do that yourself afterwards with primary set.

Reference

  • Alias API - the merge endpoints and their full request bodies
  • Alias overview - linking, resolution, and revocation