---
title: Create an account with contacts
description: Create an account, add its contacts in one batch with per-item results, and read them back, in REST or GraphQL, in cURL, TypeScript or Python.
canonical: https://developers.coevera.com/recipes/create-account-with-contacts/
section: recipes
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Create an account with contacts

**Answer:** Look up the owner by e-mail, create the account with name and owner_id, then add the contacts in one batch with account_relations, one primary relation each. Send rollback-method=1 (GraphQL ROLLBACK_FAILED) so one bad contact does not undo the others, check every item, and read the contacts back by account.

Create **Northwind Analytics** with two contacts linked to it, owned by the same sales rep. You
end with one account, two contacts and their account relations, read back from the API.

## 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). Do not relax it for new records; see [Validation](/concepts/validation/).                                                                                                                                       |
| Batch behaviour    | Step 3 is a batch sent with `rollback-method=1` (GraphQL `rollbackMethod: ROLLBACK_FAILED`): it answers 200 even if a contact fails. The valid contacts are written, and a failed one comes back as an error object. The program checks every item. |
| Context            | Region and space ID come from the context bar. Credentials come from `$COEVERA_API_KEY` and `$COEVERA_API_SECRET`.                                                                                                                                  |

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/batch-modify`](/reference/rest/contacts/#batch-modify),
[`GET /entities/Contacts`](/reference/rest/contacts/#list); GraphQL
[`ClientRepository.getByCriteria`](/reference/graphql/client/#q-getByCriteria),
[`createAccount`](/reference/graphql/account/#m-createAccount),
[`bulkUpdateContact`](/reference/graphql/contact/#m-bulkUpdateContact),
[`ContactRepository.getByCriteria`](/reference/graphql/contact/#q-getByCriteria).

## Set up a helper

Every step below uses this setup, so the steps show only what changes. 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}: ${body.message}`);
  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')}: {body.get('message')}")
    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;
}
```

**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 in `Clients`, the API
name for users. The programs encode the address: a raw `+` in a query string arrives as a space.

**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,default_unit_id \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
echo "$owner"
OWNER_ID=$(jq -er '.data[0].id' <<<"$owner")
```

**REST · TypeScript**

```ts
// URLSearchParams encodes the address: a raw "+" in a query string arrives as a space.
const lookup = new URLSearchParams({
  "filter[email]": "dana.whitfield@example.com",
  "load-only": "id,name,default_unit_id",
});
const owners = await coevera(`/entities/Clients?${lookup}`);
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,default_unit_id"},
)
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 { data: found } = await gql(`{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}`);
const ownerId: string | undefined = found.entities.client.getByCriteria.edges[0]?.node.id;
if (!ownerId) throw new Error("No user with that e-mail in this space.");
console.log("Owner:", found.entities.client.getByCriteria.edges[0].node.name, ownerId);
```

**GraphQL · Python**

```python
found = gql("""
{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}
""")
edges = found["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",
      "default_unit_id": "05000000-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.2 in the docs sandbox, de-identified._

## 2. Create the account

`name` and `owner_id` are required: without either, the API answers 422. The sales unit
(`unit_id`) defaults to the owner's default unit.

**REST · cURL**

```bash
account=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "name": "Northwind Analytics",
  "owner_id": "$OWNER_ID",
  "email1": "northwind@example.com",
  "phone1": "+1 206 555 0100",
  "home_page": "https://northwind.example.com",
  "city": "Seattle",
  "country": "United States"
}
JSON
)
echo "$account"
ACCOUNT_ID=$(jq -er '.data.id' <<<"$account")
```

**REST · TypeScript**

```ts
const account = await coevera("/entities/Accounts", {
  method: "POST",
  body: {
    name: "Northwind Analytics",
    owner_id: ownerId,
    email1: "northwind@example.com",
    phone1: "+1 206 555 0100",
    home_page: "https://northwind.example.com",
    city: "Seattle",
    country: "United States",
  },
});
const accountId: string = account.data.id;
console.log("Account:", accountId);
```

**REST · Python**

```python
account = coevera(
    "POST",
    "/entities/Accounts",
    json={
        "name": "Northwind Analytics",
        "owner_id": owner_id,
        "email1": "northwind@example.com",
        "phone1": "+1 206 555 0100",
        "home_page": "https://northwind.example.com",
        "city": "Seattle",
        "country": "United States",
    },
)
account_id = account["data"]["id"]
print("Account:", account_id)
```

**GraphQL · cURL**

```bash
account=$(jq -n --arg owner "$OWNER_ID" '{
  query: "mutation CreateAccount($input: CreateAccountInput!) { createAccount(input: $input) { account { id name owner { id name } } } }",
  variables: {
    input: {
      name: "Northwind Analytics",
      ownerId: $owner,
      email1: "northwind@example.com",
      phone1: "+1 206 555 0100",
      homePage: "https://northwind.example.com",
      city: "Seattle",
      country: "United States"
    }
  }
}' | gql)
echo "$account"
ACCOUNT_ID=$(jq -er '.data.createAccount.account.id' <<<"$account")
```

**GraphQL · TypeScript**

```ts
const { data: created } = await gql(
  `mutation CreateAccount($input: CreateAccountInput!) {
    createAccount(input: $input) {
      account { id name owner { id name } }
    }
  }`,
  {
    input: {
      name: "Northwind Analytics",
      ownerId,
      email1: "northwind@example.com",
      phone1: "+1 206 555 0100",
      homePage: "https://northwind.example.com",
      city: "Seattle",
      country: "United States",
    },
  },
);
const accountId: string = created.createAccount.account.id;
console.log("Account:", accountId);
```

**GraphQL · Python**

```python
created = gql(
    """
    mutation CreateAccount($input: CreateAccountInput!) {
      createAccount(input: $input) {
        account { id name owner { id name } }
      }
    }
    """,
    {
        "input": {
            "name": "Northwind Analytics",
            "ownerId": owner_id,
            "email1": "northwind@example.com",
            "phone1": "+1 206 555 0100",
            "homePage": "https://northwind.example.com",
            "city": "Seattle",
            "country": "United States",
        }
    },
)
account_id = created["createAccount"]["account"]["id"]
print("Account:", account_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.000040+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": "Seattle",
    "comments": "",
    "country": "United States",
    "currency_exchange_rates_list_id": null,
    "customer_type_id": null,
    "email1": "northwind@example.com",
    "email2": "",
    "email3": "",
    "email4": "",
    "email5": "",
    "health_category": null,
    "health_status": null,
    "home_page": "https://northwind.example.com",
    "industry_id": null,
    "is_archived": false,
    "last_ai_account_hierarchy_applied": null,
    "last_ai_org_chart_applied": null,
    "name": "Northwind Analytics",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "parent_account_id": null,
    "parent_account_relation_type_id": null,
    "phone1": "+1 206 555 0100",
    "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": 411,
    "formatted_name": "Northwind Analytics",
    "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": "Northwind Analytics",
        "owner": {
          "id": "0d000000-0000-4000-a000-000000000001",
          "name": "Dana Whitfield"
        }
      }
    }
  }
}
```

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

## 3. Add the contacts in one batch

Each contact links to the account through `account_relations`, with its job title in
`position`. Exactly one relation per contact must have `is_primary: true`: with none, or with
two, the contact fails validation (field error code 117). A contact also needs `first_name` or
`last_name`.

**REST · cURL**

```bash
contacts=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Contacts/batch-modify?rollback-method=1" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
[
  {
    "first_name": "Ava", "last_name": "Lindqvist",
    "email1": "ava.lindqvist@example.com",
    "owner_id": "$OWNER_ID",
    "account_relations": [
      { "account_id": "$ACCOUNT_ID", "position": "Head of Data", "is_primary": true }
    ]
  },
  {
    "first_name": "Marco", "last_name": "Ruiz",
    "email1": "marco.ruiz@example.com",
    "owner_id": "$OWNER_ID",
    "account_relations": [
      { "account_id": "$ACCOUNT_ID", "position": "CFO", "is_primary": true }
    ]
  }
]
JSON
)
echo "$contacts"
# With rollback-method=1 a failed contact comes back as an error object in data: check every item.
failed=$(jq '[.data[] | objects] | length' <<<"$contacts")
if [ "$failed" -ne 0 ]; then
  echo "$failed contact(s) failed" >&2
  exit 1
fi
```

**REST · TypeScript**

```ts
const batch = await coevera("/entities/Contacts/batch-modify?rollback-method=1", {
  method: "POST",
  body: [
    {
      first_name: "Ava",
      last_name: "Lindqvist",
      email1: "ava.lindqvist@example.com",
      owner_id: ownerId,
      account_relations: [{ account_id: accountId, position: "Head of Data", is_primary: true }],
    },
    {
      first_name: "Marco",
      last_name: "Ruiz",
      email1: "marco.ruiz@example.com",
      owner_id: ownerId,
      account_relations: [{ account_id: accountId, position: "CFO", is_primary: true }],
    },
  ],
});
// With rollback-method=1 a failed contact comes back as an error object in data: check every item.
const failed = batch.data.filter((item: unknown) => typeof item !== "string");
if (failed.length) throw new Error(`${failed.length} contact(s) failed: ${JSON.stringify(failed)}`);
console.log("Contacts:", batch.data);
```

**REST · Python**

```python
batch = coevera(
    "POST",
    "/entities/Contacts/batch-modify",
    params={"rollback-method": 1},
    json=[
        {
            "first_name": "Ava",
            "last_name": "Lindqvist",
            "email1": "ava.lindqvist@example.com",
            "owner_id": owner_id,
            "account_relations": [
                {"account_id": account_id, "position": "Head of Data", "is_primary": True}
            ],
        },
        {
            "first_name": "Marco",
            "last_name": "Ruiz",
            "email1": "marco.ruiz@example.com",
            "owner_id": owner_id,
            "account_relations": [{"account_id": account_id, "position": "CFO", "is_primary": True}],
        },
    ],
)
# With rollback-method=1 a failed contact comes back as an error object in data: check every item.
failed = [item for item in batch["data"] if not isinstance(item, str)]
if failed:
    sys.exit(f"{len(failed)} contact(s) failed: {json.dumps(failed)}")
print("Contacts:", batch["data"])
```

**GraphQL · cURL**

```bash
contacts=$(jq -n --arg owner "$OWNER_ID" --arg account "$ACCOUNT_ID" '{
  query: "mutation AddContacts($input: [CreateOrUpdateContactInput!]!) { bulkUpdateContact(input: $input, rollbackMethod: ROLLBACK_FAILED) { result { created { index id } errors { index code message } } } }",
  variables: {
    input: [
      {
        firstName: "Ava", lastName: "Lindqvist",
        email1: "ava.lindqvist@example.com",
        ownerId: $owner,
        accountRelations: [{ accountId: $account, position: "Head of Data", isPrimary: true }]
      },
      {
        firstName: "Marco", lastName: "Ruiz",
        email1: "marco.ruiz@example.com",
        ownerId: $owner,
        accountRelations: [{ accountId: $account, position: "CFO", isPrimary: true }]
      }
    ]
  }
}' | gql)
echo "$contacts"
failed=$(jq '.data.bulkUpdateContact.result.errors | length' <<<"$contacts")
if [ "$failed" -ne 0 ]; then
  echo "$failed contact(s) failed" >&2
  exit 1
fi
```

**GraphQL · TypeScript**

```ts
const { data: added } = await gql(
  `mutation AddContacts($input: [CreateOrUpdateContactInput!]!) {
    bulkUpdateContact(input: $input, rollbackMethod: ROLLBACK_FAILED) {
      result {
        created { index id }
        errors { index code message }
      }
    }
  }`,
  {
    input: [
      {
        firstName: "Ava",
        lastName: "Lindqvist",
        email1: "ava.lindqvist@example.com",
        ownerId,
        accountRelations: [{ accountId, position: "Head of Data", isPrimary: true }],
      },
      {
        firstName: "Marco",
        lastName: "Ruiz",
        email1: "marco.ruiz@example.com",
        ownerId,
        accountRelations: [{ accountId, position: "CFO", isPrimary: true }],
      },
    ],
  },
);
const { created: contactIds, errors } = added.bulkUpdateContact.result;
if (errors.length) throw new Error(`${errors.length} contact(s) failed: ${JSON.stringify(errors)}`);
console.log("Contacts:", contactIds);
```

**GraphQL · Python**

```python
added = gql(
    """
    mutation AddContacts($input: [CreateOrUpdateContactInput!]!) {
      bulkUpdateContact(input: $input, rollbackMethod: ROLLBACK_FAILED) {
        result {
          created { index id }
          errors { index code message }
        }
      }
    }
    """,
    {
        "input": [
            {
                "firstName": "Ava",
                "lastName": "Lindqvist",
                "email1": "ava.lindqvist@example.com",
                "ownerId": owner_id,
                "accountRelations": [
                    {"accountId": account_id, "position": "Head of Data", "isPrimary": True}
                ],
            },
            {
                "firstName": "Marco",
                "lastName": "Ruiz",
                "email1": "marco.ruiz@example.com",
                "ownerId": owner_id,
                "accountRelations": [{"accountId": account_id, "position": "CFO", "isPrimary": True}],
            },
        ]
    },
)
result = added["bulkUpdateContact"]["result"]
if result["errors"]:
    sys.exit(f"{len(result['errors'])} contact(s) failed: {json.dumps(result['errors'])}")
print("Contacts:", result["created"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "data": [
    "0c000000-0000-4000-a000-000000000001",
    "0c000000-0000-4000-a000-000000000002"
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "bulkUpdateContact": {
      "result": {
        "created": [
          {
            "index": 0,
            "id": "0c000000-0000-4000-a000-000000000001"
          },
          {
            "index": 1,
            "id": "0c000000-0000-4000-a000-000000000002"
          }
        ],
        "errors": []
      }
    }
  }
}
```

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

> **Warning:** Without `rollback-method=1`, one failed contact fails the whole batch and nothing is written:
> REST answers 422. In GraphQL, a `bulkUpdateContact` without `rollbackMethod` uses
> `ROLLBACK_ALL` and answers HTTP 200 with `errors` and no `data`. Send `ROLLBACK_FAILED` to get
> per-item results. [Batch operations](/concepts/batch-operations/#partial-failures) shows both
> modes.

## 4. Read them back

Filter the contacts by the account they belong to, and check them here rather than in the batch
response. REST filters on `account_relations.account_id`; GraphQL on
`accountRelations: { accountId: … }`.

**REST · cURL**

```bash
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Contacts" \
  --data-urlencode "filter[account_relations.account_id]=$ACCOUNT_ID" \
  -d load-only=id,first_name,last_name,email1 \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
```

**REST · TypeScript**

```ts
const contacts = await coevera(
  `/entities/Contacts?filter[account_relations.account_id]=${accountId}&load-only=id,first_name,last_name,email1`,
);
console.log(contacts.total, "contacts on the account:", JSON.stringify(contacts.data, null, 2));
```

**REST · Python**

```python
contacts = coevera(
    "GET",
    "/entities/Contacts",
    params={
        "filter[account_relations.account_id]": account_id,
        "load-only": "id,first_name,last_name,email1",
    },
)
print(contacts.get("total"), "contacts on the account:", json.dumps(contacts["data"], indent=2))
```

**GraphQL · cURL**

```bash
jq -n --arg account "$ACCOUNT_ID" '{
  query: "query ReadBack($account: String!) { entities { contact { getByCriteria(filter: { accountRelations: { accountId: { eq: [$account] } } }) { totalCount edges { node { id firstName lastName email1 } } } } } }",
  variables: { account: $account }
}' | gql
```

**GraphQL · TypeScript**

```ts
const { data: readBack } = await gql(
  `query ReadBack($accountId: String!) {
    entities {
      contact {
        getByCriteria(filter: { accountRelations: { accountId: { eq: [$accountId] } } }) {
          totalCount
          edges { node { id firstName lastName email1 } }
        }
      }
    }
  }`,
  { accountId },
);
const contacts = readBack.entities.contact.getByCriteria;
console.log(contacts.totalCount, "contacts on the account:", JSON.stringify(contacts.edges, null, 2));
```

**GraphQL · Python**

```python
read_back = gql(
    """
    query ReadBack($accountId: String!) {
      entities {
        contact {
          getByCriteria(filter: { accountRelations: { accountId: { eq: [$accountId] } } }) {
            totalCount
            edges { node { id firstName lastName email1 } }
          }
        }
      }
    }
    """,
    {"accountId": account_id},
)
contacts = read_back["entities"]["contact"]["getByCriteria"]
print(contacts["totalCount"], "contacts on the account:", json.dumps(contacts["edges"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 2,
  "page_info": {
    "start_cursor": "WyIwYzAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwYzAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0c000000-0000-4000-a000-000000000001",
      "email1": "ava.lindqvist@example.com",
      "first_name": "Ava",
      "last_name": "Lindqvist"
    },
    {
      "id": "0c000000-0000-4000-a000-000000000002",
      "email1": "marco.ruiz@example.com",
      "first_name": "Marco",
      "last_name": "Ruiz"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "contact": {
        "getByCriteria": {
          "totalCount": 2,
          "edges": [
            {
              "node": {
                "id": "0c000000-0000-4000-a000-000000000001",
                "firstName": "Ava",
                "lastName": "Lindqvist",
                "email1": "ava.lindqvist@example.com"
              }
            },
            {
              "node": {
                "id": "0c000000-0000-4000-a000-000000000002",
                "firstName": "Marco",
                "lastName": "Ruiz",
                "email1": "marco.ruiz@example.com"
              }
            }
          ]
        }
      }
    }
  }
}
```

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

## What to check

- Step 1 returns exactly one user. An empty `data` (GraphQL `edges`) means the e-mail is not a
  user of the space.
- Step 2 answers **201** (GraphQL: 200), and `data.id` (GraphQL `createAccount.account.id`) is
  the account ID the next steps use.
- Step 3 answers 200 with `"success": true`, and `data` holds two IDs, in the order you sent
  the contacts. With `rollback-method=1`, a failed contact makes `success` false and leaves an
  object with `code`, `name` and `message` in its place; the other contact is still written. In
  GraphQL, `result.created` lists the written items and `result.errors` the failed ones, each by
  `index`.
- Step 4 returns `total: 2` (GraphQL `totalCount: 2`). If it returns 0, the relation is
  missing: check the `account_id` you sent.
- Run the program twice and you get two accounts named Northwind Analytics and four contacts:
  the API does not refuse duplicates. To make it safe to repeat, look the account up by `name`
  first and reuse it.

## Clean up

The program leaves the account and its two contacts in your space. To remove them, send their
IDs to `POST /entities/Contacts/batch-delete` and `POST /entities/Accounts/batch-delete`, then
delete their rows in `ContactAccountRelations` the same way.

> **Note:** Deleting a contact or an account leaves its `ContactAccountRelations` rows active, still
> pointing at the deleted records. Find them with `filter[account_id]` and delete them too.
> Deletes are soft: deleted records stay readable with `include-deleted=true`.

## Limits

- The program stops at the first error and does not retry.
- The API sets no batch size limit; a batch of 500 items is accepted.
- It creates one account. For many accounts, create them in one `Accounts/batch-modify` call
  first, then send their contacts in a second batch.
