---
title: "Ask the owner of several accounts to combine them"
method: POST
path: "/stats/duplicates/cases"
tags: ["Stats"]
---

# Ask the owner of several accounts to combine them

`POST /stats/duplicates/cases`

## Request

- Base URL: `https://api.nippy.host`
- URL: `https://api.nippy.host/stats/duplicates/cases`
- Auth: `bearer` (a scheme the document does not define)

Admin-only (ADR 0057). Puts every listed account on hold — live sites go offline, nothing can be published or bought — and emails each one the three choices: combine, delete the extras, or say the accounts belong to different people. Refuses the whole list when any account is staff, banned, unverified, paid, in an organization or already in an open case.

## Request body

- OpenDuplicateCaseDto
  - `userIds` string[], required: The accounts that belong to one person. Every one is put on hold and emailed. Duplicates are harmless.
  - `note` string: The admin’s own note. Never shown to the accounts.

## Response `200`

- APIDuplicateCaseResult
  - `case` APIDuplicateCase, required
    - `id` string, required
    - `status` 'awaiting' | 'disputed' | 'resolved' | 'released', required
    - `outcome` 'combined' | 'removed', nullable, required: How a resolved case ended; null otherwise.
    - `note` string, nullable, required
    - `openedAt` string, required
    - `openedByName` string, nullable, required
    - `mainMemberId` string, nullable, required: The member the person chose to keep, once they ask to combine.
    - `combineRequestedAt` string, nullable, required
    - `disputeMessage` string, nullable, required
    - `disputedAt` string, nullable, required
    - `reminderSentAt` string, nullable, required
    - `resolvedAt` string, nullable, required: When the case was resolved or released.
    - `releasedByName` string, nullable, required
    - `converted` boolean, required: The remaining account started a subscription after the case opened and still holds it.
    - `members` APIDuplicateCaseMember[], required: Oldest account first.
      - `id` string, required
      - `userId` string, nullable, required: The user, or null once the account is combined or deleted and the user row is gone.
      - `email` string, required: The address when the case opened.
      - `state` 'pending' | 'kept' | 'combined' | 'deleted', required
      - `bannedAt` string, nullable, required
      - `heldSites` number, required: Live sites taken offline when the case opened.
      - `sitesMoved` number, nullable, required: Sites moved into the main account; null unless combined.
      - `sites` number, required: Sites the account holds now.
      - `liveSites` number, required: Of those, how many are online now.
      - `noticeSentAt` string, nullable, required: When the notice was last sent; null when the send failed.
      - `viewedAt` string, nullable, required: When this member first opened the review page.
      - `confirmSentAt` string, nullable, required: When the "confirm the combine" email went to this member.
      - `confirmedAt` string, nullable, required
      - `plan` 'free' | 'basic' | 'pro' | 'max', required: The plan the account is on now.
      - `paid` boolean, required: The account holds a live paid subscription (or trial).
  - `failures` string[], required: Steps that did not happen — a site that could not be paused, an email Postmark refused — for the admin to retry.

## Other responses

- `400`: An account cannot be in a case; the message says which.
- `403`: Admin access required

---

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