---
title: Merge duplicates
description: Merge a duplicate account into the record you keep, choose the field values that win, and see what happens to the secondary record and its relations, in REST or GraphQL.
canonical: https://developers.coevera.com/recipes/merge-duplicates/
section: recipes
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Merge duplicates

**Answer:** POST /entities/Accounts/merge with master (the record to keep, plus any values that must win) and secondary_ids. The master keeps its non-empty values and takes the secondary's for its empty fields. The secondary is soft-deleted, unchanged. Its relations move to the master but keep their modified, so a sync by modified misses the move.

**Wide World Importers** is in the space twice: once as Wide World Importers, once as Wide World
Importers Inc. with a buyer, Jonas Berg, attached. Merge the second into the first, keep a new
order e-mail, and check what the merge did to both records and to Jonas's account relation. The
program creates the two records itself; in your space you find duplicates with a lookup by name
or e-mail.

## Before you start

|                    |                                                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permissions        | **Full Access** to **Accounts** and **Contacts** on the **Records** tab of the application's permissions. The owner lookup also reads users (`Clients`). |
| `validation-level` | The default (`0`, full validation).                                                                                                                      |
| Batch behaviour    | No batches: every write is one record.                                                                                                                   |
| Context            | Region and space ID come from the context bar. Credentials come from `$COEVERA_API_KEY` and `$COEVERA_API_SECRET`.                                       |

Contacts have the same call, `POST /entities/Contacts/merge` (GraphQL `mergeContact`); this
recipe and its verification cover accounts.

The programs need `curl` and `jq`, Node.js 18 or later, or Python 3.9 or later with `requests`.
[Authentication](/get-started/authentication/) shows where the credentials come from, and
[Permissions and scopes](/get-started/permissions-and-scopes/) where to set the permissions.

The operations this recipe calls, in the API reference: REST
[`GET /entities/Clients`](/reference/rest/clients/#list),
[`POST /entities/Accounts`](/reference/rest/accounts/#create),
[`POST /entities/Contacts`](/reference/rest/contacts/#create),
[`POST /entities/Accounts/merge`](/reference/rest/accounts/#merge),
[`GET /entities/Accounts`](/reference/rest/accounts/#list),
[`GET /entities/ContactAccountRelations`](/reference/rest/contact-account-relations/#list); GraphQL
[`ClientRepository.getByCriteria`](/reference/graphql/client/#q-getByCriteria),
[`createAccount`](/reference/graphql/account/#m-createAccount),
[`createContact`](/reference/graphql/contact/#m-createContact),
[`mergeAccount`](/reference/graphql/account/#m-mergeAccount),
[`AccountRepository.getById`](/reference/graphql/account/#q-getById),
[`AccountRepository.getByCriteria`](/reference/graphql/account/#q-getByCriteria),
[`ContactAccountRelationRepository.getByCriteria`](/reference/graphql/contact-account-relation/#q-getByCriteria).

## Set up a helper

Every step below uses this setup. Export the credentials first:

```bash
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>"   # never commit it
```

**REST · cURL**

```bash
COEVERA_BASE="https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
```

**REST · TypeScript**

```ts
const base = `https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

async function coevera(path: string, init: { method?: string; body?: unknown } = {}) {
  const res = await fetch(`${base}${path}`, {
    method: init.method ?? "GET",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`HTTP ${res.status} ${body.name}: ${String(body.message).trim()}`);
  return body;
}
```

**REST · Python**

```python
BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def coevera(method, path, **kwargs):
    response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
    body = response.json()
    if not response.ok:
        sys.exit(f"HTTP {response.status_code} {body.get('name')}: {str(body.get('message')).strip()}")
    return body
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"

# gql: POST the GraphQL request read from stdin. GraphQL reports errors in the body, often
# with HTTP 200, so fail on those too.
gql() {
  local response
  response=$(curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
  if jq -e '.errors' <<<"$response" >/dev/null; then
    echo "$response" >&2
    return 1
  fi
  echo "$response"
}
```

**GraphQL · TypeScript**

```ts
const endpoint = `https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

// gql: one GraphQL request. GraphQL reports errors in the body, often with HTTP 200.
async function gql(query: string, variables: object = {}) {
  const res = await fetch(endpoint, {
    method: "POST",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  const body = await res.json();
  if (!res.ok || body.errors) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.errors ?? body)}`);
  return body.data;
}
```

**GraphQL · Python**

```python
ENDPOINT = (
    "https://{region}.api.coevera.com/api/v100/app/space/"
    "{space_id}/graphql/public"
)
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def gql(query, variables=None):
    """One GraphQL request. GraphQL reports errors in the body, often with HTTP 200."""
    response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
    body = response.json()
    if not response.ok or body.get("errors"):
        sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
    return body["data"]
```

## 1. Find the owner

Every account and contact needs an owner. Look the sales rep up by e-mail.

**REST · cURL**

```bash
owner=$(curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Clients" \
  --data-urlencode "filter[email]=dana.whitfield@example.com" \
  -d load-only=id,name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
echo "$owner"
OWNER_ID=$(jq -er '.data[0].id' <<<"$owner")
```

**REST · TypeScript**

```ts
const owners = await coevera(
  `/entities/Clients?${new URLSearchParams({ "filter[email]": "dana.whitfield@example.com", "load-only": "id,name" })}`,
);
const ownerId: string | undefined = owners.data[0]?.id;
if (!ownerId) throw new Error("No user with that e-mail in this space.");
console.log("Owner:", owners.data[0].name, ownerId);
```

**REST · Python**

```python
owners = coevera(
    "GET",
    "/entities/Clients",
    params={"filter[email]": "dana.whitfield@example.com", "load-only": "id,name"},
)
if not owners["data"]:
    sys.exit("No user with that e-mail in this space.")
owner_id = owners["data"][0]["id"]
print("Owner:", owners["data"][0]["name"], owner_id)
```

**GraphQL · cURL**

```bash
owner=$(jq -n '{
  query: "{ entities { client { getByCriteria(first: 1, filter: { email: { eq: [\"dana.whitfield@example.com\"] } }) { edges { node { id name } } } } } }"
}' | gql)
echo "$owner"
OWNER_ID=$(jq -er '.data.entities.client.getByCriteria.edges[0].node.id' <<<"$owner")
```

**GraphQL · TypeScript**

```ts
const owners = await gql(`{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}`);
const ownerId: string | undefined = owners.entities.client.getByCriteria.edges[0]?.node.id;
if (!ownerId) throw new Error("No user with that e-mail in this space.");
console.log("Owner:", owners.entities.client.getByCriteria.edges[0].node.name, ownerId);
```

**GraphQL · Python**

```python
owners = gql("""
{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}
""")
edges = owners["entities"]["client"]["getByCriteria"]["edges"]
if not edges:
    sys.exit("No user with that e-mail in this space.")
owner_id = edges[0]["node"]["id"]
print("Owner:", edges[0]["node"]["name"], owner_id)
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwZDAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwZDAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0d000000-0000-4000-a000-000000000001",
      "name": "Dana Whitfield"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "client": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0d000000-0000-4000-a000-000000000001",
                "name": "Dana Whitfield"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 2. Create the record to keep

The master: it has a sales e-mail and a phone number, but no city or comments.

**REST · cURL**

```bash
# Two records for one company, each with values the other lacks. This one is kept.
master=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "name": "Wide World Importers", "owner_id": "$OWNER_ID",
  "email1": "wideworld.sales@example.com", "phone1": "+1 206 555 0120"
}
JSON
)
echo "$master"
MASTER_ID=$(jq -er '.data.id' <<<"$master")
```

**REST · TypeScript**

```ts
// Two records for one company, each with values the other lacks. This one is kept.
const keep = await coevera("/entities/Accounts", {
  method: "POST",
  body: { name: "Wide World Importers", owner_id: ownerId, email1: "wideworld.sales@example.com", phone1: "+1 206 555 0120" },
});
const masterId: string = keep.data.id;
console.log("Master:", masterId);
```

**REST · Python**

```python
# Two records for one company, each with values the other lacks. This one is kept.
keep = coevera(
    "POST",
    "/entities/Accounts",
    json={
        "name": "Wide World Importers",
        "owner_id": owner_id,
        "email1": "wideworld.sales@example.com",
        "phone1": "+1 206 555 0120",
    },
)
master_id = keep["data"]["id"]
print("Master:", master_id)
```

**GraphQL · cURL**

```bash
# Two records for one company, each with values the other lacks. This one is kept.
master=$(jq -n --arg owner "$OWNER_ID" '{
  query: "mutation CreateAccount($input: CreateAccountInput!) { createAccount(input: $input) { account { id name } } }",
  variables: {
    input: { name: "Wide World Importers", ownerId: $owner,
      email1: "wideworld.sales@example.com", phone1: "+1 206 555 0120" }
  }
}' | gql)
echo "$master"
MASTER_ID=$(jq -er '.data.createAccount.account.id' <<<"$master")
```

**GraphQL · TypeScript**

```ts
// Two records for one company, each with values the other lacks. This one is kept.
const CREATE_ACCOUNT = `mutation CreateAccount($input: CreateAccountInput!) {
  createAccount(input: $input) { account { id name } }
}`;
const keep = await gql(CREATE_ACCOUNT, {
  input: { name: "Wide World Importers", ownerId, email1: "wideworld.sales@example.com", phone1: "+1 206 555 0120" },
});
const masterId: string = keep.createAccount.account.id;
console.log("Master:", masterId);
```

**GraphQL · Python**

```python
# Two records for one company, each with values the other lacks. This one is kept.
CREATE_ACCOUNT = """
mutation CreateAccount($input: CreateAccountInput!) {
  createAccount(input: $input) { account { id name } }
}
"""
keep = gql(
    CREATE_ACCOUNT,
    {
        "input": {
            "name": "Wide World Importers",
            "ownerId": owner_id,
            "email1": "wideworld.sales@example.com",
            "phone1": "+1 206 555 0120",
        }
    },
)
master_id = keep["createAccount"]["account"]["id"]
print("Master:", master_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "0a000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.000000+00:00",
    "created": "2026-10-15 09:00:00.000033+00:00",
    "account_type": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/AccountTypes/03000000-0000-4000-a000-000000000001",
    "currency_exchange_rates_list": null,
    "customer_type": null,
    "industry": null,
    "owner": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Clients/0d000000-0000-4000-a000-000000000001",
    "parent_account": null,
    "parent_account_relation_type": null,
    "picture": null,
    "unit": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/SalesUnits/05000000-0000-4000-a000-000000000001",
    "account_class": 1,
    "account_type_id": "03000000-0000-4000-a000-000000000001",
    "address": "",
    "city": "",
    "comments": "",
    "country": "",
    "currency_exchange_rates_list_id": null,
    "customer_type_id": null,
    "email1": "wideworld.sales@example.com",
    "email2": "",
    "email3": "",
    "email4": "",
    "email5": "",
    "health_category": null,
    "health_status": null,
    "home_page": "",
    "industry_id": null,
    "is_archived": false,
    "last_ai_account_hierarchy_applied": null,
    "last_ai_org_chart_applied": null,
    "name": "Wide World Importers",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "parent_account_id": null,
    "parent_account_relation_type_id": null,
    "phone1": "+1 206 555 0120",
    "phone2": "",
    "phone3": "",
    "phone4": "",
    "phone5": "",
    "picture_id": null,
    "quick_parent_account_name": "",
    "share_mode": 0,
    "state_province": "",
    "unit_id": "05000000-0000-4000-a000-000000000001",
    "zip_code": "",
    "revision": 1433,
    "formatted_name": "Wide World Importers",
    "social_media": null,
    "sharing_units": [],
    "sharing_clients": [],
    "documents": [],
    "is_favorite": false,
    "is_unsubscribed": false,
    "tags": [],
    "static_profiles": [],
    "health": null,
    "modified_by_user": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createAccount": {
      "account": {
        "id": "0a000000-0000-4000-a000-000000000001",
        "name": "Wide World Importers"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 3. Create the duplicate

The secondary: another e-mail and phone number, and a city and comments the master lacks.

**REST · cURL**

```bash
duplicate=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "name": "Wide World Importers Inc.", "owner_id": "$OWNER_ID",
  "email1": "buying.wideworld@example.com", "phone1": "+1 206 555 0129",
  "city": "Salem", "comments": "Imported from the trade-show list."
}
JSON
)
echo "$duplicate"
SECONDARY_ID=$(jq -er '.data.id' <<<"$duplicate")
```

**REST · TypeScript**

```ts
const duplicate = await coevera("/entities/Accounts", {
  method: "POST",
  body: {
    name: "Wide World Importers Inc.",
    owner_id: ownerId,
    email1: "buying.wideworld@example.com",
    phone1: "+1 206 555 0129",
    city: "Salem",
    comments: "Imported from the trade-show list.",
  },
});
const secondaryId: string = duplicate.data.id;
console.log("Duplicate:", secondaryId);
```

**REST · Python**

```python
duplicate = coevera(
    "POST",
    "/entities/Accounts",
    json={
        "name": "Wide World Importers Inc.",
        "owner_id": owner_id,
        "email1": "buying.wideworld@example.com",
        "phone1": "+1 206 555 0129",
        "city": "Salem",
        "comments": "Imported from the trade-show list.",
    },
)
secondary_id = duplicate["data"]["id"]
print("Duplicate:", secondary_id)
```

**GraphQL · cURL**

```bash
duplicate=$(jq -n --arg owner "$OWNER_ID" '{
  query: "mutation CreateAccount($input: CreateAccountInput!) { createAccount(input: $input) { account { id name } } }",
  variables: {
    input: { name: "Wide World Importers Inc.", ownerId: $owner,
      email1: "buying.wideworld@example.com", phone1: "+1 206 555 0129",
      city: "Salem", comments: "Imported from the trade-show list." }
  }
}' | gql)
echo "$duplicate"
SECONDARY_ID=$(jq -er '.data.createAccount.account.id' <<<"$duplicate")
```

**GraphQL · TypeScript**

```ts
const duplicate = await gql(CREATE_ACCOUNT, {
  input: {
    name: "Wide World Importers Inc.",
    ownerId,
    email1: "buying.wideworld@example.com",
    phone1: "+1 206 555 0129",
    city: "Salem",
    comments: "Imported from the trade-show list.",
  },
});
const secondaryId: string = duplicate.createAccount.account.id;
console.log("Duplicate:", secondaryId);
```

**GraphQL · Python**

```python
duplicate = gql(
    CREATE_ACCOUNT,
    {
        "input": {
            "name": "Wide World Importers Inc.",
            "ownerId": owner_id,
            "email1": "buying.wideworld@example.com",
            "phone1": "+1 206 555 0129",
            "city": "Salem",
            "comments": "Imported from the trade-show list.",
        }
    },
)
secondary_id = duplicate["createAccount"]["account"]["id"]
print("Duplicate:", secondary_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "0a000000-0000-4000-a000-000000000002",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.336684+00:00",
    "created": "2026-10-15 09:00:00.336718+00:00",
    "account_type": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/AccountTypes/03000000-0000-4000-a000-000000000001",
    "currency_exchange_rates_list": null,
    "customer_type": null,
    "industry": null,
    "owner": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Clients/0d000000-0000-4000-a000-000000000001",
    "parent_account": null,
    "parent_account_relation_type": null,
    "picture": null,
    "unit": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/SalesUnits/05000000-0000-4000-a000-000000000001",
    "account_class": 1,
    "account_type_id": "03000000-0000-4000-a000-000000000001",
    "address": "",
    "city": "Salem",
    "comments": "Imported from the trade-show list.",
    "country": "",
    "currency_exchange_rates_list_id": null,
    "customer_type_id": null,
    "email1": "buying.wideworld@example.com",
    "email2": "",
    "email3": "",
    "email4": "",
    "email5": "",
    "health_category": null,
    "health_status": null,
    "home_page": "",
    "industry_id": null,
    "is_archived": false,
    "last_ai_account_hierarchy_applied": null,
    "last_ai_org_chart_applied": null,
    "name": "Wide World Importers Inc.",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "parent_account_id": null,
    "parent_account_relation_type_id": null,
    "phone1": "+1 206 555 0129",
    "phone2": "",
    "phone3": "",
    "phone4": "",
    "phone5": "",
    "picture_id": null,
    "quick_parent_account_name": "",
    "share_mode": 0,
    "state_province": "",
    "unit_id": "05000000-0000-4000-a000-000000000001",
    "zip_code": "",
    "revision": 1434,
    "formatted_name": "Wide World Importers Inc.",
    "social_media": null,
    "sharing_units": [],
    "sharing_clients": [],
    "documents": [],
    "is_favorite": false,
    "is_unsubscribed": false,
    "tags": [],
    "static_profiles": [],
    "health": null,
    "modified_by_user": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createAccount": {
      "account": {
        "id": "0a000000-0000-4000-a000-000000000002",
        "name": "Wide World Importers Inc."
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 4. Add a contact to the duplicate

Jonas Berg works at the secondary record, as its primary account. The merge moves this relation.

**REST · cURL**

```bash
contact=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Contacts" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "first_name": "Jonas", "last_name": "Berg",
  "email1": "jonas.berg@example.com",
  "owner_id": "$OWNER_ID",
  "account_relations": [{ "account_id": "$SECONDARY_ID", "position": "Buyer", "is_primary": true }]
}
JSON
)
echo "$contact"
CONTACT_ID=$(jq -er '.data.id' <<<"$contact")
```

**REST · TypeScript**

```ts
const contact = await coevera("/entities/Contacts", {
  method: "POST",
  body: {
    first_name: "Jonas",
    last_name: "Berg",
    email1: "jonas.berg@example.com",
    owner_id: ownerId,
    account_relations: [{ account_id: secondaryId, position: "Buyer", is_primary: true }],
  },
});
const contactId: string = contact.data.id;
console.log("Contact on the secondary:", contactId);
```

**REST · Python**

```python
contact = coevera(
    "POST",
    "/entities/Contacts",
    json={
        "first_name": "Jonas",
        "last_name": "Berg",
        "email1": "jonas.berg@example.com",
        "owner_id": owner_id,
        "account_relations": [{"account_id": secondary_id, "position": "Buyer", "is_primary": True}],
    },
)
contact_id = contact["data"]["id"]
print("Contact on the secondary:", contact_id)
```

**GraphQL · cURL**

```bash
contact=$(jq -n --arg owner "$OWNER_ID" --arg account "$SECONDARY_ID" '{
  query: "mutation AddContact($input: CreateContactInput!) { createContact(input: $input) { contact { id } } }",
  variables: {
    input: {
      firstName: "Jonas", lastName: "Berg",
      email1: "jonas.berg@example.com",
      ownerId: $owner,
      accountRelations: [{ accountId: $account, position: "Buyer", isPrimary: true }]
    }
  }
}' | gql)
echo "$contact"
CONTACT_ID=$(jq -er '.data.createContact.contact.id' <<<"$contact")
```

**GraphQL · TypeScript**

```ts
const contact = await gql(
  `mutation AddContact($input: CreateContactInput!) {
    createContact(input: $input) { contact { id } }
  }`,
  {
    input: {
      firstName: "Jonas",
      lastName: "Berg",
      email1: "jonas.berg@example.com",
      ownerId,
      accountRelations: [{ accountId: secondaryId, position: "Buyer", isPrimary: true }],
    },
  },
);
const contactId: string = contact.createContact.contact.id;
console.log("Contact on the secondary:", contactId);
```

**GraphQL · Python**

```python
contact = gql(
    """
    mutation AddContact($input: CreateContactInput!) {
      createContact(input: $input) { contact { id } }
    }
    """,
    {
        "input": {
            "firstName": "Jonas",
            "lastName": "Berg",
            "email1": "jonas.berg@example.com",
            "ownerId": owner_id,
            "accountRelations": [{"accountId": secondary_id, "position": "Buyer", "isPrimary": True}],
        }
    },
)
contact_id = contact["createContact"]["contact"]["id"]
print("Contact on the secondary:", contact_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "0c000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.757279+00:00",
    "created": "2026-10-15 09:00:00.724431+00:00",
    "contact_type": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/ContactTypes/00000000-0000-4000-a000-000000000001",
    "currency_exchange_rates_list": null,
    "owner": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Clients/0d000000-0000-4000-a000-000000000001",
    "picture": null,
    "unit": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/SalesUnits/05000000-0000-4000-a000-000000000001",
    "address": "",
    "city": "",
    "comments": "",
    "contact_type_id": "00000000-0000-4000-a000-000000000001",
    "country": "",
    "currency_exchange_rates_list_id": null,
    "email1": "jonas.berg@example.com",
    "email2": "",
    "email3": "",
    "email4": "",
    "email5": "",
    "first_name": "Jonas",
    "gender": 0,
    "is_archived": false,
    "last_name": "Berg",
    "middle_name": "",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "phone1": "",
    "phone2": "",
    "phone3": "",
    "phone4": "",
    "phone5": "",
    "picture_id": null,
    "position": "",
    "quick_account_name": "",
    "share_mode": 0,
    "state_province": "",
    "title": "",
    "unit_id": "05000000-0000-4000-a000-000000000001",
    "zip_code": "",
    "revision": 1435,
    "formatted_name": "Jonas Berg",
    "social_media": null,
    "primary_account": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Accounts/0a000000-0000-4000-a000-000000000002",
    "account_relations": [
      "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/ContactAccountRelations/0e000000-0000-4000-a000-000000000001"
    ],
    "sharing_units": [],
    "sharing_clients": [],
    "documents": [],
    "exchange_entity_id": null,
    "gapi_entity_id": null,
    "o365_entity_id": null,
    "is_favorite": false,
    "primary_account_account_roles": [],
    "primary_account_position": "Buyer",
    "account_position": "Buyer",
    "is_unsubscribed": false,
    "tags": [],
    "static_profiles": [],
    "primary_account_relationship": 0,
    "modified_by_user": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createContact": {
      "contact": {
        "id": "0c000000-0000-4000-a000-000000000001"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 5. Merge

Send `master` and `secondary_ids`. `master` takes the record's `id` and any field values that must
win over both records, here a new `email1`. REST takes the secondary records as a list in
`secondary_ids`; GraphQL `mergeAccount` takes one, in `slaveId`. The call answers 200 with the
master record.

`master` takes the fields an update takes. A read-only field, or a name the entity does not
have, fails the whole call with HTTP 400, code 117 `ERROR_PARAMETER_INVALID`, and nothing is
merged.

**REST · cURL**

```bash
# master names the record to keep and carries the values that must win; the secondary
# records in secondary_ids are merged into it.
curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts/merge" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "master": { "id": "$MASTER_ID", "email1": "orders.wideworld@example.com" },
  "secondary_ids": ["$SECONDARY_ID"]
}
JSON
echo
```

**REST · TypeScript**

```ts
// master names the record to keep and carries the values that must win; the secondary
// records in secondary_ids are merged into it.
const merged = await coevera("/entities/Accounts/merge", {
  method: "POST",
  body: { master: { id: masterId, email1: "orders.wideworld@example.com" }, secondary_ids: [secondaryId] },
});
console.log("Merged into:", merged.data.id);
```

**REST · Python**

```python
# master names the record to keep and carries the values that must win; the secondary
# records in secondary_ids are merged into it.
merged = coevera(
    "POST",
    "/entities/Accounts/merge",
    json={"master": {"id": master_id, "email1": "orders.wideworld@example.com"}, "secondary_ids": [secondary_id]},
)
print("Merged into:", merged["data"]["id"])
```

**GraphQL · cURL**

```bash
# master names the record to keep and carries the values that must win; slaveId is the one
# secondary record merged into it.
jq -n --arg master "$MASTER_ID" --arg secondary "$SECONDARY_ID" '{
  query: "mutation Merge($input: MergeAccountInput!) { mergeAccount(input: $input) { result { id name } } }",
  variables: { input: { master: { id: $master, email1: "orders.wideworld@example.com" }, slaveId: $secondary } }
}' | gql
```

**GraphQL · TypeScript**

```ts
// master names the record to keep and carries the values that must win; slaveId is the one
// secondary record merged into it.
const merged = await gql(
  `mutation Merge($input: MergeAccountInput!) {
    mergeAccount(input: $input) { result { id name } }
  }`,
  { input: { master: { id: masterId, email1: "orders.wideworld@example.com" }, slaveId: secondaryId } },
);
console.log("Merged into:", merged.mergeAccount.result.id);
```

**GraphQL · Python**

```python
# master names the record to keep and carries the values that must win; slaveId is the one
# secondary record merged into it.
merged = gql(
    """
    mutation Merge($input: MergeAccountInput!) {
      mergeAccount(input: $input) { result { id name } }
    }
    """,
    {"input": {"master": {"id": master_id, "email1": "orders.wideworld@example.com"}, "slaveId": secondary_id}},
)
print("Merged into:", merged["mergeAccount"]["result"]["id"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "0a000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:07.153829+00:00",
    "created": "2026-10-15 09:00:00.000033+00:00",
    "account_type": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/AccountTypes/03000000-0000-4000-a000-000000000001",
    "currency_exchange_rates_list": null,
    "customer_type": null,
    "industry": null,
    "owner": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Clients/0d000000-0000-4000-a000-000000000001",
    "parent_account": null,
    "parent_account_relation_type": null,
    "picture": null,
    "unit": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/SalesUnits/05000000-0000-4000-a000-000000000001",
    "account_class": 1,
    "account_type_id": "03000000-0000-4000-a000-000000000001",
    "address": "",
    "city": "Salem",
    "comments": "Imported from the trade-show list.",
    "country": "",
    "currency_exchange_rates_list_id": null,
    "customer_type_id": null,
    "email1": "orders.wideworld@example.com",
    "email2": "",
    "email3": "",
    "email4": "",
    "email5": "",
    "health_category": null,
    "health_status": null,
    "home_page": "",
    "industry_id": null,
    "is_archived": false,
    "last_ai_account_hierarchy_applied": null,
    "last_ai_org_chart_applied": null,
    "name": "Wide World Importers",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "parent_account_id": null,
    "parent_account_relation_type_id": null,
    "phone1": "+1 206 555 0120",
    "phone2": "",
    "phone3": "",
    "phone4": "",
    "phone5": "",
    "picture_id": null,
    "quick_parent_account_name": "",
    "share_mode": 0,
    "state_province": "",
    "unit_id": "05000000-0000-4000-a000-000000000001",
    "zip_code": "",
    "revision": 1436,
    "formatted_name": "Wide World Importers",
    "social_media": null,
    "sharing_units": [],
    "sharing_clients": [],
    "documents": [],
    "is_favorite": false,
    "is_unsubscribed": false,
    "tags": [],
    "static_profiles": [],
    "health": null,
    "modified_by_user": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "mergeAccount": {
      "result": {
        "id": "0a000000-0000-4000-a000-000000000001",
        "name": "Wide World Importers"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

What the merge does to the values:

- The master keeps its own non-empty values: `name` and `phone1`.
- An empty master field takes the secondary's value: `city` and `comments`.
- A value sent in `master` wins over both records: `email1`.

| Field      | Master before                 | Secondary                          | Master after                       |
| ---------- | ----------------------------- | ---------------------------------- | ---------------------------------- |
| `name`     | Wide World Importers          | Wide World Importers Inc.          | Wide World Importers               |
| `phone1`   | +1 206 555 0120               | +1 206 555 0129                    | +1 206 555 0120                    |
| `city`     | (empty)                       | Salem                              | Salem                              |
| `comments` | (empty)                       | Imported from the trade-show list. | Imported from the trade-show list. |
| `email1`   | `wideworld.sales@example.com` | `buying.wideworld@example.com`     | `orders.wideworld@example.com`     |

## 6. Read the master back

**REST · cURL**

```bash
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
  -d "filter[id]=$MASTER_ID" \
  -d load-only=id,name,email1,phone1,city,comments,modified \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echo
```

**REST · TypeScript**

```ts
const master = await coevera(
  `/entities/Accounts?filter[id]=${masterId}&load-only=id,name,email1,phone1,city,comments,modified`,
);
console.log("Master:", JSON.stringify(master.data[0], null, 2));
```

**REST · Python**

```python
master = coevera(
    "GET",
    "/entities/Accounts",
    params={"filter[id]": master_id, "load-only": "id,name,email1,phone1,city,comments,modified"},
)
print("Master:", json.dumps(master["data"][0], indent=2))
```

**GraphQL · cURL**

```bash
jq -n --arg id "$MASTER_ID" '{
  query: "query ReadMaster($id: ID!) { entities { account { getById(entityId: $id) { id name email1 phone1 city comments modified } } } }",
  variables: { id: $id }
}' | gql
```

**GraphQL · TypeScript**

```ts
const master = await gql(
  `query ReadMaster($id: ID!) {
    entities { account { getById(entityId: $id) { id name email1 phone1 city comments modified } } }
  }`,
  { id: masterId },
);
console.log("Master:", JSON.stringify(master.entities.account.getById, null, 2));
```

**GraphQL · Python**

```python
master = gql(
    """
    query ReadMaster($id: ID!) {
      entities { account { getById(entityId: $id) { id name email1 phone1 city comments modified } } }
    }
    """,
    {"id": master_id},
)
print("Master:", json.dumps(master["entities"]["account"]["getById"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0a000000-0000-4000-a000-000000000001",
      "modified": "2026-10-15 09:00:07.153829+00:00",
      "city": "Salem",
      "comments": "Imported from the trade-show list.",
      "email1": "orders.wideworld@example.com",
      "name": "Wide World Importers",
      "phone1": "+1 206 555 0120"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "account": {
        "getById": {
          "id": "0a000000-0000-4000-a000-000000000001",
          "name": "Wide World Importers",
          "email1": "orders.wideworld@example.com",
          "phone1": "+1 206 555 0120",
          "city": "Salem",
          "comments": "Imported from the trade-show list.",
          "modified": "2026-10-15T09:00:00.909865+00:00"
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 7. Read the secondary back

The secondary is soft-deleted (`is_deleted: true`; it is not archived), and its own values are
untouched. A list call shows it only with `include-deleted=true` (GraphQL `includeDeleted: true`);
`GET /entities/Accounts/{id}` still answers 200 for it.

**REST · cURL**

```bash
# The secondary is soft-deleted: list calls show it only with include-deleted=true.
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
  -d "filter[id]=$SECONDARY_ID" -d include-deleted=true \
  -d load-only=id,name,email1,phone1,city,is_deleted \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echo
```

**REST · TypeScript**

```ts
// The secondary is soft-deleted: list calls show it only with include-deleted=true.
const secondary = await coevera(
  `/entities/Accounts?filter[id]=${secondaryId}&include-deleted=true&load-only=id,name,email1,phone1,city,is_deleted`,
);
console.log("Secondary:", JSON.stringify(secondary.data[0], null, 2));
```

**REST · Python**

```python
# The secondary is soft-deleted: list calls show it only with include-deleted=true.
secondary = coevera(
    "GET",
    "/entities/Accounts",
    params={
        "filter[id]": secondary_id,
        "include-deleted": "true",
        "load-only": "id,name,email1,phone1,city,is_deleted",
    },
)
print("Secondary:", json.dumps(secondary["data"][0], indent=2))
```

**GraphQL · cURL**

```bash
# The secondary is soft-deleted: it shows only with includeDeleted: true.
jq -n --arg id "$SECONDARY_ID" '{
  query: "query ReadSecondary($id: String!) { entities { account { getByCriteria(includeDeleted: true, filter: { id: { eq: [$id] } }) { edges { node { id name email1 phone1 city isDeleted } } } } } }",
  variables: { id: $id }
}' | gql
```

**GraphQL · TypeScript**

```ts
// The secondary is soft-deleted: it shows only with includeDeleted: true.
const secondary = await gql(
  `query ReadSecondary($id: String!) {
    entities {
      account {
        getByCriteria(includeDeleted: true, filter: { id: { eq: [$id] } }) {
          edges { node { id name email1 phone1 city isDeleted } }
        }
      }
    }
  }`,
  { id: secondaryId },
);
console.log("Secondary:", JSON.stringify(secondary.entities.account.getByCriteria.edges, null, 2));
```

**GraphQL · Python**

```python
# The secondary is soft-deleted: it shows only with includeDeleted: true.
secondary = gql(
    """
    query ReadSecondary($id: String!) {
      entities {
        account {
          getByCriteria(includeDeleted: true, filter: { id: { eq: [$id] } }) {
            edges { node { id name email1 phone1 city isDeleted } }
          }
        }
      }
    }
    """,
    {"id": secondary_id},
)
print("Secondary:", json.dumps(secondary["entities"]["account"]["getByCriteria"]["edges"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ==",
    "end_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0a000000-0000-4000-a000-000000000002",
      "is_deleted": true,
      "city": "Salem",
      "email1": "buying.wideworld@example.com",
      "name": "Wide World Importers Inc.",
      "phone1": "+1 206 555 0129"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "account": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000002",
                "name": "Wide World Importers Inc.",
                "email1": "buying.wideworld@example.com",
                "phone1": "+1 206 555 0129",
                "city": "Salem",
                "isDeleted": true
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 8. Read the contact's relation

The secondary's relations move to the master in place: Jonas's `ContactAccountRelations` row keeps
its `id`, `is_primary` and `position`, and its `account_id` now names the master.

**REST · cURL**

```bash
# The contact's relation now points at the master, with its old modified.
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/ContactAccountRelations" \
  -d "filter[contact_id]=$CONTACT_ID" \
  -d load-only=id,account_id,position,is_primary,modified \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
```

**REST · TypeScript**

```ts
// The contact's relation now points at the master, with its old modified.
const relations = await coevera(
  `/entities/ContactAccountRelations?filter[contact_id]=${contactId}&load-only=id,account_id,position,is_primary,modified`,
);
console.log("Relation:", JSON.stringify(relations.data, null, 2));
```

**REST · Python**

```python
# The contact's relation now points at the master, with its old modified.
relations = coevera(
    "GET",
    "/entities/ContactAccountRelations",
    params={"filter[contact_id]": contact_id, "load-only": "id,account_id,position,is_primary,modified"},
)
print("Relation:", json.dumps(relations["data"], indent=2))
```

**GraphQL · cURL**

```bash
# The contact's relation now points at the master, with its old modified.
jq -n --arg contact "$CONTACT_ID" '{
  query: "query ReadRelation($contact: String!) { entities { contactAccountRelation { getByCriteria(filter: { contactId: { eq: [$contact] } }) { edges { node { id accountId position isPrimary modified } } } } } }",
  variables: { contact: $contact }
}' | gql
```

**GraphQL · TypeScript**

```ts
// The contact's relation now points at the master, with its old modified.
const relations = await gql(
  `query ReadRelation($contactId: String!) {
    entities {
      contactAccountRelation {
        getByCriteria(filter: { contactId: { eq: [$contactId] } }) {
          edges { node { id accountId position isPrimary modified } }
        }
      }
    }
  }`,
  { contactId },
);
console.log("Relation:", JSON.stringify(relations.entities.contactAccountRelation.getByCriteria.edges, null, 2));
```

**GraphQL · Python**

```python
# The contact's relation now points at the master, with its old modified.
relations = gql(
    """
    query ReadRelation($contactId: String!) {
      entities {
        contactAccountRelation {
          getByCriteria(filter: { contactId: { eq: [$contactId] } }) {
            edges { node { id accountId position isPrimary modified } }
          }
        }
      }
    }
    """,
    {"contactId": contact_id},
)
print("Relation:", json.dumps(relations["entities"]["contactAccountRelation"]["getByCriteria"]["edges"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwZTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwZTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0e000000-0000-4000-a000-000000000001",
      "modified": "2026-10-15 09:00:00.741041+00:00",
      "account_id": "0a000000-0000-4000-a000-000000000001",
      "is_primary": true,
      "position": "Buyer"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "contactAccountRelation": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0e000000-0000-4000-a000-000000000001",
                "accountId": "0a000000-0000-4000-a000-000000000001",
                "position": "Buyer",
                "isPrimary": true,
                "modified": "2026-10-15T09:00:00.000000+00:00"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

> **Warning:** A re-pointed relation keeps its `modified`, and so does the contact. An integration that syncs
> changes by `modified` never sees the contact move to the master. After a merge, read the master's
> relations again, or re-read the records you track.

## What to check

- Step 5 answers **200** with the master's `id`.
- Step 6 shows the kept, filled and sent values from the table above.
- Step 7 returns the secondary with `is_deleted: true` and its own `email1` and `name`.
- Step 8 returns one relation whose `account_id` is the master and whose `modified` is older than
  the master's.
- Run the program twice and you get a second pair, merged the same way. To make a merge job
  safe to repeat, check that the secondary is not already deleted (`is_deleted`) before you
  merge.

## Clean up

The program leaves the master account, the soft-deleted secondary and the contact in your space.
Delete the contact, then the master, with `batch-delete`, and then the contact's
`ContactAccountRelations` row: deleting a contact or an account leaves it active.

## Limits

- The program merges one pair. REST takes a list of ids in `secondary_ids`; GraphQL takes one
  per call.
