# Northwind CRM — API Northwind CRM is a Salesforce-shaped. Its vocabulary is **Contacts · Accounts · Opportunities · Cases**. A record is a Contact identified by `ContactId` (for example `003Ax000001aBcD`). Base URL: `https://necogoode-systems.azurewebsites.net` Authentication: none (demo system, public). Content type: `application/json`. ## Identifiers this system emits | Identifier | Example | Notes | |---|---|---| | `ContactId` | 003Ax000001aBcD | the CRM record id | | `Email` | nadia.hollis@northwind-traders.example | the person's work email | > Northwind CRM does **not** hold employee numbers or asset tags. Any such field sent to this API is dropped and reported back in `droppedFields`. ## Endpoints ### `GET /api/crm/records` List every Contact, newest first. ```bash curl -s https://necogoode-systems.azurewebsites.net/api/crm/records ``` ```json { "ok": true, "count": 1, "records": [ { "ContactId": "003Ax000001aBcD", "createdAt": "2026-08-29T09:14:02.113Z", "eventCount": 2, "lastDelivery": { "httpStatus": 202, "receiptStatus": "accepted", "receiptReason": null } } ] } ``` ### `POST /api/crm/records` Create a Contact. **Emits `northwind.contact.created`.** ```bash curl -s -X POST https://necogoode-systems.azurewebsites.net/api/crm/records \ -H 'content-type: application/json' \ -d '{ "FirstName": "Nadia", "LastName": "Hollis", "Email": "nadia.hollis@northwind-traders.example", "AccountName": "Northwind Traders", "Department": "Operations", "LineManager": "A. Whitfield", "EquipmentPackage": "standard", "Stage": "Contact" }' ``` ```json { "ok": true, "record": { "ContactId": "003Ax000001aBcD", ... }, "event": { "eventType": "northwind.contact.created", "externalEventId": "crm-003Ax000001aBcD-0001", "delivery": { "httpStatus": 202, "receiptStatus": "accepted", "receiptReason": null } } } ``` Optional body keys on every write: `occurredAt` (ISO — backdates the record and the event) and `emit: false` (suppress the webhook). A create missing a required field returns `400 { "ok": false, "error": "missing required field(s): ..." }`. Identifiers belonging to another system are dropped before the record is written and returned in `droppedFields`. #### Body fields | Field | Type | Required | Notes | |---|---|---|---| | `FirstName` | string | yes | First name | | `LastName` | string | yes | Last name | | `Email` | string | yes | Email | | `AccountName` | string | no | Account | | `Department` | string (enum) | no | Department — one of: `Finance`, `Operations`, `Sales`, `Information Technology`, `Human Resources`, `Logistics` | | `JobTitle` | string | no | Job title | | `LineManager` | string (enum) | no | Line manager — one of: `A. Whitfield`, `R. Okonkwo`, `S. Delacroix`, `M. Bergstrom`, `P. Ramachandran`, `J. Halvorsen`, `T. Nakamura`, `C. Mbeki`, `L. Fontaine` | | `StartDate` | string | no | Start date | | `EquipmentPackage` | string (enum) | no | Equipment package — one of: `standard`, `specialist` | | `CostCentre` | string | no | Cost centre (optional) | | `Stage` | string (enum) | no | Stage — one of: `Contact`, `Employee` | | `CaseNotes` | string | no | Case notes | ### `GET /api/crm/records/{id}` Read one Contact. 404 if unknown. ### `PATCH /api/crm/records/{id}` Update a Contact. **Emits `northwind.contact.updated`** with a new `externalEventId` and the same `ContactId`. The payload carries `changedFields`, an array of the field names supplied. ```bash curl -s -X PATCH https://necogoode-systems.azurewebsites.net/api/crm/records/003Ax000001aBcD \ -H 'content-type: application/json' -d '{"Department":"Finance","CostCentre":"CC-4120"}' ``` ### `POST /api/crm/records/{id}/convert` Convert the Contact record to an Employee record on its Account. **Emits `northwind.contact.converted`.** ```bash curl -s -X POST https://necogoode-systems.azurewebsites.net/api/crm/records/003Ax000001aBcD/convert \ -H 'content-type: application/json' -d '{"employeeType":"Permanent"}' ``` | Body field | Type | Required | Notes | |---|---|---|---| | `employeeType` | string | no | one of: `Permanent`, `Contract`, `Intern` | ### `GET /api/crm/records/{id}/events` Every event Northwind CRM has emitted for that record, oldest first, each with the SoloLift delivery outcome. ### `GET /api/crm/events?limit=200` Every event this system has emitted, newest first. ### `POST /api/crm/chaos` Emit a deliberately awkward event so anomaly detection can be exercised. `{ "mode": "out_of_order" | "duplicate" | "omit_field" | "unknown_type", "recordId": "003Ax000001aBcD" }` | Mode | What it does | |---|---| | `out_of_order` | emits `northwind.contact.updated` stamped 36h **before** the record was created | | `duplicate` | replays the record's last event verbatim, reusing its `externalEventId` | | `omit_field` | emits an update whose payload is missing the normally-present `Email` | | `unknown_type` | emits `northwind.contact.teleported`, a type this system does not normally emit | ### `POST /api/crm/seed` Generate backdated history. `{ "count": 20, "from": "2026-06-20T00:00:00Z", "to": "2026-08-29T00:00:00Z", "includeAction": true }` Optional: `fieldRatios` (probability an optional field is populated, e.g. `{"CostCentre":0.35}`), `specialistRatio`, `template` (values forced onto every seeded record), `emit:false`. ### `GET /api/_health` `{ "ok": true, "build": "" }` ## Events emitted | Event type | When | Payload fields | |---|---|---| | `northwind.contact.created` | a Contact is created | `ContactId`, `FirstName`, `LastName`, `Email`, `AccountName`, `Department`, `JobTitle`, `LineManager`, `StartDate`, `EquipmentPackage`, `CostCentre`, `Stage`, `CaseNotes`, `createdAt` | | `northwind.contact.updated` | a Contact is edited or re-submitted | as above, plus `changedFields` | | `northwind.contact.converted` | convert to employee | as above, plus `previousStage`, `newStage`, `EmployeeType`, `ConvertedAt` | | `northwind.contact.teleported` | chaos only — never emitted in normal operation | as above | Each emission POSTs to `{SOLOLIFT_BASE_URL}/api/v1/events/ingest` with `x-sololift-signature: sha256=` and the envelope `{ eventSourceId, externalEventId, eventType, subject, occurredAt, payload }`. `externalEventId` is `crm--<4-digit sequence>`.