# Contacts

Create, read, and update customer and supplier contacts.

Canonical HTML: https://developer.shelfcycle.com/guides/contacts

List, read, create, and update supported contacts under existing customers or suppliers.

## List contacts

```text
GET /contacts
```

Required scope:

```text
contacts:read
```

`GET /contacts` requires `parentType` and `parentId`. It is not a broad contact export.

```bash
curl "$SHELFCYCLE_API_BASE_URL/contacts?parentType=customer&parentId=customer-id&limit=10" \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY"
```

Optional filters are `q`, `limit`, and `includeArchived`.

## Create a contact

```text
POST /contacts
```

Required scope:

```text
contacts:create
```

Required header:

```text
Idempotency-Key: <stable-key>
```

Add `dryRun=true` to check the exact intended contact write without mutating records.

Create a contact under an existing customer or supplier. ShelfCycle rejects archived parents, cross-org ids, and exact duplicates under the same parent.

```bash
curl "$SHELFCYCLE_API_BASE_URL/contacts" \
  -X POST \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gmail-thread-123:customer-id:create-contact" \
  -d '{
    "parent": { "type": "customer", "id": "customer-id" },
    "name": "Jane Buyer",
    "title": "Purchasing",
    "email": "jane@example.com",
    "phone": "555-0100",
    "mobilePhone": "555-0101",
    "faxNumber": "555-0102",
    "isEmergency": true,
    "documentTypes": ["PRICE_QUOTE", "INVOICE"]
  }'
```

```json
{
  "data": {
    "type": "contact",
    "id": "contact-id",
    "contactKind": "customer",
    "displayName": "Jane Buyer",
    "subtitle": "jane@example.com | Purchasing | 555-0100",
    "url": "https://app.shelfcycle.com/org-northstar/customers/customer-id/contacts",
    "parent": { "type": "customer", "id": "customer-id", "displayName": "Northstar Chemical" },
    "createdAt": "2026-05-29T14:30:00.000Z",
    "updatedAt": "2026-05-29T14:30:00.000Z",
    "name": "Jane Buyer",
    "title": "Purchasing",
    "email": "jane@example.com",
    "phone": "555-0100",
    "mobilePhone": "555-0101",
    "faxNumber": "555-0102",
    "isEmergency": true,
    "documentTypes": ["PRICE_QUOTE", "INVOICE"],
    "idempotencyStatus": "created"
  }
}
```

Repeating the same route plus `Idempotency-Key` with the same request body in the same org returns the existing contact with `idempotencyStatus: "replayed"`, even after key rotation.

## Read a contact

```text
GET /contacts/{id}
```

Required scope:

```text
contacts:read
```

For v1 compatibility and optimistic-concurrency readback, `search:read` or `contacts:update` can also read a visible contact by id. The response includes the latest `updatedAt` value to use as `If-Match`.

## Update a contact

```text
PATCH /contacts/{id}
```

Required scope:

```text
contacts:update
```

Required header:

```text
If-Match: <updatedAt from latest response or GET /contacts/{id}>
```

Sparse patch fields:

```json
{
  "name": "Jane Buyer",
  "title": "Director of Purchasing",
  "email": "jane@example.com",
  "phone": "555-0101",
  "mobilePhone": "555-0102",
  "faxNumber": null,
  "isEmergency": false,
  "documentTypes": ["SALES_CONFIRMATION"]
}
```

Omitted fields stay unchanged. `title`, `email`, `phone`, `mobilePhone`, and `faxNumber` can be sent as `null` to clear. `name` cannot be cleared.

Supplier contacts support `title`, `email`, `phone`, `isEmergency`, and supplier document types such as `PURCHASE_ORDERS` and `LOGISTICS`. They reject `mobilePhone` and `faxNumber` because the supplier-contact model has no matching fields.

If the record changed after the `updatedAt` value sent in `If-Match`, ShelfCycle returns `409 stale_record`.

> **Guardrail**: Parent reassignment, archive state, broad export, and hidden fields are not supported through v1.21.
