Contacts

Create and update approved contacts under existing customers or suppliers.

Create a contact

POST /contacts

Required scope:

contacts:create

Required header:

Idempotency-Key: <stable-key>

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

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"]
  }'
{
  "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

GET /contacts/{id}

Required scope:

search:read

The response includes the latest updatedAt value to use as If-Match.

Update a contact

PATCH /contacts/{id}

Required scope:

contacts:update

Required header:

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

Sparse patch fields:

{
  "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, and hidden fields are not supported through v1.