Contacts
List, read, create, and update supported contacts under existing customers or suppliers.
List contacts
GET /contacts
Required scope:
contacts:read
GET /contacts requires parentType and parentId. It is not a broad contact export.
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
POST /contacts
Required scope:
contacts:create
Required header:
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.
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:
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
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.
Parent reassignment, archive state, broad export, and hidden fields are not supported through v1.21.