---
title: "Invert a merge, restoring exactly the references it moved"
method: POST
path: "/v1/merges/{id}/unmerge"
tags: ["merges"]
---

# Invert a merge, restoring exactly the references it moved

`POST /v1/merges/{id}/unmerge`

## Request

- Base URL: `https://api.indraft.io`
- URL: `https://api.indraft.io/v1/merges/{id}/unmerge`
- Auth: HTTP bearer

Invert a merge, restoring exactly the references it moved. Refused if the merge has already been undone.

## Path parameters

- `id` string, required

## Request body

- object
  - `idempotencyKey` string: Your own name for this write, so retrying it cannot do it twice. Send the same key again and you get the SAME result back rather than a second record, which is what makes a re-run safe after a timeout you never saw the answer to. Derive it from what the write is about, not from the clock: `nightly-sync-halden` retries correctly and a timestamp does not.
  - `expectedVersion` integer: The version you read before deciding to write. If the record has moved since, the write is refused instead of overwriting whatever changed. Send it whenever you read, reasoned, and then wrote: without it the last writer wins and neither of you finds out.
  - `sourceId` string: The recorded source this claim came from, if one exists. It attaches evidence to the write, so a person reading the field later can see where it came from rather than only who sent it.
  - `assertionKind` 'observed' | 'inferred' | 'imported' | 'user_supplied': HOW YOU KNOW. This is the field the product is built around, so say what is true rather than what is strongest: `observed` you saw it stated directly, `imported` it came from a file or another system, `inferred` you worked it out, `user_supplied` a person told you in so many words. They rank, weakest first: inferred < imported < observed < user_supplied < confirmed. A weaker claim does not overwrite a stronger one; it is reported in `held[]` and the stored value stands, so an unattended re-run converges instead of eroding what somebody checked. `confirmed` is not accepted here: it means a person checked the value, and only a signed-in person can set it.
  - `attributionLabel` string: A short name for the work this write belongs to, shown beside the actor so one credential can distinguish separate jobs. It is recorded as CALLER-SUPPLIED and grants nothing: it changes no permission, no idempotency scope and no quota.
  - `dryRun` boolean: Report what this write WOULD do and commit nothing. The response carries the same change list, held fields and refusals as the real call, so it is the way to check a risky write, or a whole import, before it lands. Nothing is metered.
  - `overrideStanding` boolean: Apply this write even when the value standing in the record was asserted more strongly than this one. Absent means no, which is the safe answer for a re-run. WITHOUT IT, a write can be accepted and change nothing: precedence is confirmed > user_supplied > observed > imported > inferred, and a weaker claim leaves the standing value in place and reports the field in `held[]` rather than failing. That is the whole of why a write can return 200 and not take. WITH it, this write wins regardless of what it is overruling, INCLUDING a value a person confirmed, so an agent that sets it by default has switched the provenance mechanism off. Set it per write, when the caller knows its claim supersedes what is there.

## Response `200`

Success

- object
  - `record` object
    - `mergeId` string, required
  - `mutation` object, required
    - `mutationId` string, required
    - `status` 'created' | 'updated' | 'matched' | 'archived' | 'merged' | 'reverted' | 'noop', required: What the write did. `noop` means it was accepted and changed nothing, because every value it carried already matched what was stored. IT IS NOT METERED: a noop does not count against your monthly change allowance, so a nightly sync that finds nothing new costs nothing. Neither does a write that only REDUCES what a workspace holds. `matched` means an upsert found an existing record on real identity evidence rather than creating a second one; `reverted` and `merged` are the undo and duplicate paths, and both are ordinary writes that count.
    - `targetId` string
    - `record` unknown
    - `changes` object[]
      - `field` string, required
      - `before` unknown
      - `after` unknown
    - `held` object[]: Fields this write asked to change and did not, because the value standing in the record was asserted more strongly. THIS IS WHY A WRITE CAN RETURN 200 AND CHANGE NOTHING, and a caller that ignores it will believe it wrote something it did not. Precedence is confirmed > user_supplied > observed > imported > inferred, and a write carrying no `assertionKind` is weakest of all. Each entry reports the field, what the record still says, what you proposed, which claim won, and who made it. Send `overrideStanding` to write anyway, per write, when your claim really does supersede theirs.
      - `field` string, required
      - `kept` unknown
      - `proposed` unknown
      - `standing` 'observed' | 'inferred' | 'imported' | 'user_supplied' | 'confirmed', required
      - `incoming` 'observed' | 'inferred' | 'imported' | 'user_supplied' | 'confirmed', required
      - `actorId` string, required
      - `assertedAt` string, required
    - `matchedBy` 'email' | 'domain' | 'external_id' | 'idempotency_key' | 'explicit_id'
    - `reversible` boolean, required
    - `replayed` boolean
    - `dryRun` boolean
  - `requestId` string, required

## Other responses

- `401`: No credential, or one that has been revoked.
- `402`: A product quota is spent, or the subscription is not active.
- `403`: Authenticated, and this capability is not held.
- `404`: No such record, or it belongs to another workspace.
- `409`: A conflict the caller can resolve: a stale expectedVersion, an idempotency key reused for different input, an ambiguous or duplicate identity, or a merge that no longer applies.
- `413`: The body, or a single record inside it, exceeds its bound.
- `422`: The request does not satisfy the schema on this page.
- `429`: Infrastructure protection tripped. This is not a product quota.
- `500`: A defect on our side. The hint says so, and carries an incident id.
- `507`: The workspace is at its storage ceiling.

---

[API](https://skmtc.dev/indraft/apis/indraft-api.md) · [All operations](https://skmtc.dev/indraft/apis/indraft-api/llms.txt) · [OpenAPI document](https://skmtc.dev/indraft/apis/indraft-api/revisions/d9bb05346d66?raw)
