# Signal Trade CRM API helpers (Node.js)

Import the helpers from the [Node.js sync client](./signal-trade-stock-sync.mjs). They use the approved API base URL and bearer key; keep the key in an environment variable and never commit it.

```js
import { randomUUID } from 'node:crypto';
import {
  loadCrmAssignees,
  createCrmRecord,
  updateCrmRecord,
  createCrmTask,
  updateCrmTask,
} from './signal-trade-stock-sync.mjs';

const config = {
  baseUrl: process.env.SIGNAL_TRADE_BASE_URL || 'https://signalautomotive.uk',
  apiKey: process.env.SIGNAL_TRADE_API_KEY,
};
const assignees = await loadCrmAssignees(config);
console.log(assignees.map(({ userId, displayName, role }) => ({ userId, displayName, role })));

const task = await createCrmTask({
  ...config,
  createId: randomUUID(),
  task: {
    title: 'Call seller about service history',
    dueDate: '2026-10-02',
    ownerUserId: assignees[0]?.userId ?? null,
  },
});

// Pass the current task version and only changed fields.
const updated = await updateCrmTask({
  ...config,
  taskId: task.id,
  version: task.version,
  changes: { ownerUserId: null },
});
```

The generic helpers also create customers, opportunities and activity entries. Use the ID returned from each create to link records in your business workspace:

```js
const customer = await createCrmRecord({
  ...config,
  kind: 'customers',
  createId: randomUUID(),
  record: { name: 'Example seller', role: 'seller', phone: '02890000000' },
});

const opportunity = await createCrmRecord({
  ...config,
  kind: 'opportunities',
  createId: randomUUID(),
  record: {
    title: 'AB12CDE · vehicle purchase',
    direction: 'buy',
    customerId: customer.id,
    stage: 'new',
    nextAction: 'Arrange a viewing',
  },
});

await createCrmRecord({
  ...config,
  kind: 'activities',
  createId: randomUUID(),
  record: {
    customerId: customer.id,
    opportunityId: opportunity.id,
    kind: 'call',
    summary: 'Spoke with the seller; viewing requested.',
  },
});

const revisedOpportunity = await updateCrmRecord({
  ...config,
  kind: 'opportunities',
  recordId: opportunity.id,
  version: opportunity.version,
  changes: { stage: 'contacted' },
});
```

Customer, opportunity and task records can be updated with `updateCrmRecord`; activity entries are append-only and must not be updated. Creates require a new UUID v4 `createId`. If a create request times out, retry only the exact same record details with the same ID. Updates are not retried automatically; after a timeout or HTTP 409, fetch the current record and reconcile using its latest `version`.

The directory omits email addresses. `crm:read` is sufficient to read it; writing CRM records requires `crm:manage`. Writes are not automatically retried because a timeout can leave the outcome uncertain. For a create, retry only the same details with the same UUID `createId`. After an update timeout or HTTP 409, read that customer, opportunity or task again and reconcile before sending another update.

## Book an appointment

Select the staff member from `loadCrmAssignees` and retain their stable `userId`. An untimed reminder needs no duration. A timed appointment needs a date, UK local start time, a whole duration from 1 to 720 minutes and a current staff assignment:

```js
const selectedUserId = 'REPLACE_WITH_SELECTED_STAFF_ID';
const appointmentCreateId = randomUUID(); // Keep this ID with this exact payload for a retry.
const appointment = await createCrmTask({
  ...config,
  createId: appointmentCreateId,
  task: {
    title: 'Seller appraisal',
    kind: 'viewing',
    dueDate: '2026-10-05',
    dueTime: '10:00',
    durationMinutes: 45,
    ownerUserId: selectedUserId,
    // Add customerId, vehicleId and siteId when the appointment is linked.
  },
});
```

Appointments must finish by midnight on their recorded date. Signal prevents overlapping open appointments for the same staff member across branches or the same vehicle across staff. Back-to-back appointments are allowed. HTTP 409 `CRM_CONFLICT` means reload the relevant records and select another slot. A timed legacy record with no duration conservatively blocks that resource for its recorded day; record its length or complete it before booking another appointment. Reopening or rescheduling a timed appointment requires a known duration and current staff member. Local times are Europe/London wall-clock values; this contract does not provide UTC instants, travel buffers, staff working hours or branch bay capacity.
