# Contacts · Management API

> Management API reference for contacts: GET /sites/{id}/contacts, POST /sites/{id}/contacts, GET /sites/{id}/contacts/{contactId}, PATCH /sites/{id}/contacts/{contactId}, ….

> Markdown version of https://kanorio.com/en/developers/management-api/contacts. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp

Base URL: `https://app.kanorio.com/api/v1` · [Errors](https://kanorio.com/en/developers/basics/errors) · [Pagination](https://kanorio.com/en/developers/basics/pagination)

### List contacts

`GET /sites/{id}/contacts`

- Stability: beta
- Permission: `customers:view`
- Rate limit: default (120/minute per token)
- MCP tool: `contacts_list`

List contacts with email marketing status, tags and purchase summary. Filter by status, source, buyers only, or search on email/name/phone; sort by RECENT, SPENT or LAST_ORDER. Errors: FORBIDDEN, INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `limit` | query | integer | no | Maximum number of items to return, 1–100. Defaults to 20. |
| `cursor` | query | string | no | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
| `search` | query | string | no | — |
| `status` | query | "ALL" \| "NEVER_SUBSCRIBED" \| "PENDING" \| "SUBSCRIBED" \| "UNSUBSCRIBED" \| "BOUNCED" \| "COMPLAINED" | no | — |
| `source` | query | "ALL" \| "CHECKOUT" \| "ORDER" \| "IMPORT" \| "FORM" \| "MANUAL" \| "ADMIN" | no | — |
| `buyer` | query | "ALL" \| "BUYERS" | no | BUYERS = has at least one paid order. |
| `sort` | query | "RECENT" \| "SPENT" \| "LAST_ORDER" | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].email` | string \| null | yes | — |
| `data[].phone` | string \| null | yes | E.164. |
| `data[].name` | string \| null | yes | — |
| `data[].emailStatus` | string | yes | Email marketing consent: NEVER_SUBSCRIBED, PENDING, SUBSCRIBED, UNSUBSCRIBED, BOUNCED, COMPLAINED. |
| `data[].source` | string | yes | — |
| `data[].createdAt` | string | yes | — |
| `data[].tags` | string[] | yes | — |
| `data[].customer` | object \| null | yes | EC purchase summary; null when the contact never bought anything. |
| `data[].customer.ordersCount` | integer | yes | — |
| `data[].customer.totalSpent` | integer | yes | Minor units in the website currency (orders in other currencies are not included). |
| `data[].customer.lastOrderAt` | string \| null | yes | — |
| `hasMore` | boolean | yes | True when another page exists; pass nextCursor to fetch it. |
| `nextCursor` | string \| null | yes | Cursor for the next page, or null on the last page. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `INVALID_CURSOR` | 400 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/contacts" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "data": [
    {
      "id": "contact_example01",
      "email": "customer@example.com",
      "phone": "+886912345678",
      "name": "Alex Chen",
      "emailStatus": "SUBSCRIBED",
      "source": "FORM",
      "createdAt": "2026-10-01T02:30:00.000Z",
      "tags": [
        "VIP",
        "cake-order"
      ],
      "accessStatus": "ACTIVE",
      "externalId": null,
      "externalIdOrigin": null,
      "metadata": {},
      "customer": {
        "ordersCount": 3,
        "totalSpent": 2640,
        "lastOrderAt": "2026-10-06T08:00:00.000Z"
      }
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Add a contact

`POST /sites/{id}/contacts`

- Stability: beta
- Permission: `customers:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `contacts_add`

Add one contact by email (source MANUAL). Only set marketingConsent=true when the person has explicitly agreed to receive marketing email; otherwise the contact is created without consent. externalId and metadata let you link the contact to your own system. Errors: FORBIDDEN, VALIDATION, DUPLICATE (email already exists), SUPPRESSED (address is blocked), INVALID_EMAIL, INVALID_PHONE, EXTERNAL_ID_IN_USE.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | — |
| `name` | string | no | — |
| `phone` | string | no | Any common format; normalized to E.164 using the store's country. |
| `tags` | string[] | no | — |
| `note` | string | no | — |
| `marketingConsent` | boolean | no | Defaults to false. |
| `externalId` | string | no | Your system's id for this person (POS, ERP, CRM). Unique per site and set once; it is never used to match or merge contacts. |
| `externalIdOrigin` | string | no | Name of the system the externalId comes from, e.g. "shopify", "hubspot". |
| `metadata` | object | no | Integration fields to set on the new contact. |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contactId` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `VALIDATION` | 400 | — |
| `DUPLICATE` | 409 | email already exists |
| `SUPPRESSED` | 422 | address is blocked |
| `INVALID_EMAIL` | 400 | — |
| `INVALID_PHONE` | 400 | — |
| `EXTERNAL_ID_IN_USE` | 422 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/contacts" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"customer@example.com","name":"Alex Chen","tags":["cake-order"],"marketingConsent":false}'
```

```json
{
  "contactId": "contact_example01"
}
```

### Retrieve a contact

`GET /sites/{id}/contacts/{contactId}`

- Stability: beta
- Permission: `customers:view`
- Rate limit: default (120/minute per token)
- MCP tool: `contacts_get`

Get one contact by contactId or by externalId (your system's id) with consent state, sign-in status, note, custom profile attributes, metadata, purchase summary with recent orders, and the last 50 timeline events/messages. Errors: FORBIDDEN, VALIDATION, CONTACT_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `contactId` | path | string | yes | Contact id. Get it from contacts.list. |
| `externalId` | query | string | no | Look the contact up by your system's id instead of contactId. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `email` | string \| null | yes | — |
| `phone` | string \| null | yes | E.164. |
| `name` | string \| null | yes | — |
| `emailStatus` | string | yes | Email marketing consent: NEVER_SUBSCRIBED, PENDING, SUBSCRIBED, UNSUBSCRIBED, BOUNCED, COMPLAINED. |
| `source` | string | yes | — |
| `createdAt` | string | yes | — |
| `tags` | string[] | yes | — |
| `customer` | object \| null | yes | — |
| `customer.id` | string | yes | — |
| `customer.ordersCount` | integer | yes | — |
| `customer.totalSpent` | integer | yes | — |
| `customer.lastOrderAt` | string \| null | yes | — |
| `customer.acceptsMarketing` | boolean | yes | — |
| `customer.country` | string \| null | yes | — |
| `customer.recentOrders` | object[] | yes | — |
| `customer.recentOrders[].id` | string | yes | — |
| `customer.recentOrders[].orderNumber` | integer \| null | yes | — |
| `customer.recentOrders[].status` | string | yes | — |
| `customer.recentOrders[].paidAmount` | integer | yes | — |
| `customer.recentOrders[].currency` | string | yes | — |
| `customer.recentOrders[].createdAt` | string | yes | — |
| `locale` | string \| null | yes | — |
| `emailConsentAt` | string \| null | yes | — |
| `emailUnsubscribedAt` | string \| null | yes | — |
| `note` | string | yes | — |
| `accessStatus` | "ACTIVE" \| "BLOCKED" \| "PENDING" | yes | Sign-in status. BLOCKED: the person cannot sign in to the account portal; orders and emails are unaffected. |
| `externalId` | string \| null | yes | Your system's id for this person; null when none was set. |
| `externalIdOrigin` | string \| null | yes | Which system the externalId comes from. |
| `metadata` | object | yes | Integration fields (flat key/value, up to 50 keys). |
| `suppressed` | boolean | yes | On the tenant suppression list (bounce/complaint/manual block). |
| `profile` | object[] | yes | Custom attributes collected via forms. |
| `profile[].key` | string | yes | — |
| `profile[].label` | string | yes | — |
| `profile[].value` | string | yes | — |
| `consentLogs` | object[] | yes | — |
| `consentLogs[].id` | string | yes | — |
| `consentLogs[].channel` | string | yes | — |
| `consentLogs[].action` | string | yes | — |
| `consentLogs[].source` | string | yes | — |
| `consentLogs[].createdAt` | string | yes | — |
| `events` | object[] | yes | — |
| `events[].id` | string | yes | — |
| `events[].type` | string | yes | — |
| `events[].occurredAt` | string | yes | — |
| `events[].payload` | any | yes | — |
| `messages` | object[] | yes | — |
| `messages[].id` | string | yes | — |
| `messages[].channel` | string | yes | — |
| `messages[].kind` | string | yes | — |
| `messages[].type` | string | yes | — |
| `messages[].status` | string | yes | — |
| `messages[].at` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `VALIDATION` | 400 | — |
| `CONTACT_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/contacts/{contactId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "id": "contact_example01",
  "email": "customer@example.com",
  "phone": "+886912345678",
  "name": "Alex Chen",
  "emailStatus": "SUBSCRIBED",
  "source": "FORM",
  "createdAt": "2026-10-01T02:30:00.000Z",
  "tags": [
    "VIP",
    "cake-order"
  ],
  "accessStatus": "ACTIVE",
  "externalId": null,
  "externalIdOrigin": null,
  "metadata": {},
  "customer": {
    "id": "customer_example01",
    "ordersCount": 3,
    "totalSpent": 2640,
    "lastOrderAt": "2026-10-06T08:00:00.000Z",
    "acceptsMarketing": true,
    "country": "TW",
    "recentOrders": [
      {
        "id": "order_example01",
        "orderNumber": 1024,
        "status": "PAID",
        "paidAmount": 980,
        "currency": "TWD",
        "createdAt": "2026-10-06T08:00:00.000Z"
      }
    ]
  },
  "locale": "zh-Hant",
  "emailConsentAt": "2026-10-01T02:30:00.000Z",
  "emailUnsubscribedAt": null,
  "note": "Prefers less sugar.",
  "suppressed": false,
  "profile": [
    {
      "key": "birthday",
      "label": "Birthday",
      "value": "10-20"
    }
  ],
  "consentLogs": [
    {
      "id": "consent_example01",
      "channel": "EMAIL",
      "action": "SUBSCRIBE",
      "source": "FORM",
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  ],
  "events": [
    {
      "id": "evt_example01",
      "type": "form.submitted",
      "occurredAt": "2026-10-01T02:30:00.000Z",
      "payload": {
        "formId": "form_example01"
      }
    }
  ],
  "messages": [
    {
      "id": "msg_example01",
      "channel": "EMAIL",
      "kind": "CAMPAIGN",
      "type": "campaign",
      "status": "DELIVERED",
      "at": "2026-10-06T08:00:00.000Z"
    }
  ]
}
```

### Update a contact

`PATCH /sites/{id}/contacts/{contactId}`

- Stability: beta
- Permission: `customers:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `contacts_update`

Update a contact's name, tags, owner note, externalId and/or metadata (provide at least one; the others keep their current value). Tags are trimmed, de-duplicated, max 20 × 50 chars. externalId can be set once. metadata is merged: keys you send are set, null deletes a key. Email and phone are identities and are not editable. Errors: FORBIDDEN, VALIDATION, CONTACT_NOT_FOUND, EXTERNAL_ID_ALREADY_SET, EXTERNAL_ID_IN_USE.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `contactId` | path | string | yes | Contact id. Get it from contacts.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Display name: printed on certificates and used in email greetings. Empty string clears it (the email address is shown instead). Email and phone are identities and cannot be changed here. |
| `tags` | string[] | no | — |
| `note` | string | no | — |
| `externalId` | string | no | Set once; re-sending the same value is a no-op, a different value fails with EXTERNAL_ID_ALREADY_SET. |
| `externalIdOrigin` | string | no | Name of the system the externalId comes from, e.g. "shopify", "hubspot". |
| `metadata` | object | no | Integration fields. Keys you send are set, a null value deletes that key, keys you omit are kept. Up to 50 keys / 16 KB after merging. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contactId` | string | yes | — |
| `name` | string \| null | yes | — |
| `tags` | string[] | yes | — |
| `note` | string | yes | — |
| `externalId` | string \| null | yes | — |
| `metadata` | object | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `VALIDATION` | 400 | — |
| `CONTACT_NOT_FOUND` | 404 | — |
| `EXTERNAL_ID_ALREADY_SET` | 422 | — |
| `EXTERNAL_ID_IN_USE` | 422 | — |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/contacts/{contactId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alex Chen","tags":["VIP","cake-order"],"note":"Prefers less sugar."}'
```

```json
{
  "contactId": "contact_example01",
  "name": "Alex Chen",
  "tags": [
    "VIP",
    "cake-order"
  ],
  "note": "Prefers less sugar.",
  "externalId": null,
  "metadata": {}
}
```

### Update tags note

`PATCH /sites/{id}/contacts/{contactId}/tags-note`

- Stability: deprecated (sunset 2027-04-30, use contacts.update)
- Permission: `customers:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `contacts_update_tags_note`

[Deprecated, removed after 2027-04-30; use contacts.update instead. contacts.update covers the same fields and can also set the contact's name, which certificates print and emails use as the greeting.] Deprecated alias of contacts.update (also accepts name, externalId and metadata). Errors: FORBIDDEN, VALIDATION, CONTACT_NOT_FOUND, EXTERNAL_ID_ALREADY_SET, EXTERNAL_ID_IN_USE.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `contactId` | path | string | yes | Contact id. Get it from contacts.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Display name: printed on certificates and used in email greetings. Empty string clears it (the email address is shown instead). Email and phone are identities and cannot be changed here. |
| `tags` | string[] | no | — |
| `note` | string | no | — |
| `externalId` | string | no | Set once; re-sending the same value is a no-op, a different value fails with EXTERNAL_ID_ALREADY_SET. |
| `externalIdOrigin` | string | no | Name of the system the externalId comes from, e.g. "shopify", "hubspot". |
| `metadata` | object | no | Integration fields. Keys you send are set, a null value deletes that key, keys you omit are kept. Up to 50 keys / 16 KB after merging. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contactId` | string | yes | — |
| `name` | string \| null | yes | — |
| `tags` | string[] | yes | — |
| `note` | string | yes | — |
| `externalId` | string \| null | yes | — |
| `metadata` | object | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `VALIDATION` | 400 | — |
| `CONTACT_NOT_FOUND` | 404 | — |
| `EXTERNAL_ID_ALREADY_SET` | 422 | — |
| `EXTERNAL_ID_IN_USE` | 422 | — |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/contacts/{contactId}/tags-note" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tags":["VIP","cake-order"],"note":"Prefers less sugar."}'
```

```json
{
  "contactId": "contact_example01",
  "name": "Alex Chen",
  "tags": [
    "VIP",
    "cake-order"
  ],
  "note": "Prefers less sugar.",
  "externalId": null,
  "metadata": {}
}
```
