Three small reference-data domains that share the standard upsert pipeline. Read The Upsert Model first.
Source of truth: ServerMethodsCategory.pas / CM_Category.pas / AO_Category.pas, ServerMethodsUnit.pas / CM_Unit.pas / AO_Unit.pas, ServerMethodsKeyword.pas / CM_Keyword.pas / AO_Keyword.pas.
Categories
URL class: Category — /Windward/WebAPI/Category/...
| Verb | Path | Delphi method |
|---|---|---|
| GET | /Category/Category_Handshake | Category_Handshake |
| GET | /Category/Categories/{CategoryNumber} | Categories |
| POST | /Category/addCategories | updateaddCategories |
What a category number is
Categories are identified by a System Five ledger-style number, not an integer id: a dotted numeric string such as 01.02. The API parses it through StringToLed and validates it with LedCheck.
This is the only domain in the API whose primary key is a string path parameter.
GET /Category/Categories/{CategoryNumber}
- Pass a category number to fetch one category.
- To fetch all categories, pass the literal string
undefinedas the path segment (this is what the Swagger description specifies):GET /Windward/WebAPI/Category/Categories/undefined
Supports Fields. It does not support pagination — an all-categories fetch returns everything in one response.
POST /Category/addCategories
{
"ConnectionInfo": { "TerminalNumber": 1 },
"Categories": [
{ "CategoryNumber": "01.02", "CategoryName": "Widgets",
"CategoryType": "I", "Income": "4010.000", "Expense": "5010.000",
"Income2": "", "Expense2": "" }
]
}Match rule
Only CategoryNumber (key 0). Present and valid -> update if it exists, insert if it does not. Blank or unparseable -> skipped:
Category number missing, record skipped
Because the match is on the number alone, this endpoint is fully idempotent: re-posting the same category number updates in place. There is no way to create a second category with the same number, and no way to change a category's number (that would create a new one).
Fields
| Field | Type | Notes |
|---|---|---|
CategoryNumber | string | Ledger-style, e.g. 01.02. Required |
CategoryName | string | Used at creation |
CategoryType | string | Category type |
Income, Income2 | string | Income ledger accounts |
Expense, Expense2 | string | Expense ledger accounts |
ActionResult is Inserted Record / Updated Record / An error occurred during the insert of the record / An error occurred during the update of the record.
Supports Fields and DetailedResponse=Y.
Units
URL class: Units — /Windward/WebAPI/Units/...
Units are serialised equipment records (vehicles, machines) tracked individually.
| Verb | Path | Delphi method |
|---|---|---|
| GET | /Units/Unit_Handshake | Unit_Handshake |
| GET | /Units/Units/{UnitId} | Units |
| POST | /Units/addUnit | updateaddUnit |
| POST | /Units/UpdateFreeFormHeaders | updateUpdateFreeFormHeaders |
As with vendors, the GET path doubles the segment: /Windward/WebAPI/Units/Units/{id}.
GET /Units/Units/{UnitId}
0 returns all units. Supports Fields and FreeFormNameMap=Y. No pagination.
POST /Units/addUnit
{
"ConnectionInfo": { "TerminalNumber": 1 },
"Units": [
{ "UnitId": 0, "UnitNo": "TRK-014", "Serial": "1FTFW1E5XKF00001",
"Make": "Ford", "Model": "F-150", "Year": 2024, "License": "ABC-123",
"UnitFreeFormGroup": [ { "FreeFormID": 1, "FreeFormData": "Fleet A" } ] }
]
}Match rule — three attempts
| Order | Field | Index |
|---|---|---|
| 1 | UnitId | key 0 |
| 2 | Serial | key 2 |
| 3 | UnitNo | key 7 |
Serialis a match key. Re-posting a unit with a known serial number updates the existing unit rather than creating a duplicate. This is usually what you want for equipment, but it means a mistyped or reused serial silently overwrites another unit's record.
Serial is the recommended idempotency key where serials are genuinely unique.
Fields
| Field | Type | Notes |
|---|---|---|
UnitId | integer | Match key 1 |
Serial | string | Match key 2 |
UnitNo | string | Match key 3 |
Make, Model, License | string | |
Year | integer | |
UnitFreeFormGroup | array | Free-form values by id |
Supports Fields and DetailedResponse=Y.
POST /Units/UpdateFreeFormHeaders
The only endpoint in the API that writes free-form header labels rather than values. Elsewhere free-form headers are read-only (FreeFormNameMap=Y).
{
"ConnectionInfo": { "TerminalNumber": 1 },
"UnitFreeFormHeaders": [
{ "1": "Fleet Code", "FreeFormHeaderIsChanged": true },
{ "2": "Service Due", "FreeFormHeaderIsChanged": false }
]
}The FreeFormHeaderIsChanged flag is mandatory for a write
Each entry is an object whose key is the free-form id (as a string) and whose value is the new label, plus a FreeFormHeaderIsChanged boolean.
Only entries with FreeFormHeaderIsChanged: true are written. Entries with false, or without the flag, are read and ignored. This lets you round-trip the map you read from FreeFormNameMap=Y and mark only the ones you changed.
Response
An ad hoc APIResponse-style object rather than a record array:
| Outcome | IsSuccess | Response |
|---|---|---|
| One or more headers written | true | Free Form Headers Processed Successfully |
| Payload present but nothing marked changed / no valid entries | false | No valid Free Form Header data received |
| No payload | false | No Free Form Header data received |
RecordCount is the number of headers actually written.
Free-form headers are dataset-wide configuration, shared by every unit. Changing one relabels the field everywhere.
Keywords
URL class: Keyword — /Windward/WebAPI/Keyword/...
Keywords are the free-text codes attached to invoice lines (job codes, categories of work).
| Verb | Path | Delphi method |
|---|---|---|
| GET | /Keyword/Keyword_Handshake | Keyword_Handshake |
| GET | /Keyword/Keywords/{KeywordId} | Keywords |
| POST | /Keyword/addKeyword | updateaddKeyword |
GET /Keyword/Keywords/{KeywordId}
0 returns all keywords. Supports Fields. No pagination.
POST /Keyword/addKeyword
{
"ConnectionInfo": { "TerminalNumber": 1 },
"Keywords": [ { "Unique": 0, "Sort": "SVC", "Word": "Service call" } ]
}Match rule — three attempts
| Order | Field | Index |
|---|---|---|
| 1 | Unique | key 0 |
| 2 | Sort | key 1 |
| 3 | Word | key 2 |
Both Sort and Word are match keys, so either can serve as an idempotency key. Equally, changing a keyword's Word while keeping its Sort will update the record matched on Sort — you cannot use this endpoint to create a second keyword sharing a sort code.
Fields
| Field | Type | Notes |
|---|---|---|
Unique | integer | Match key 1 |
Sort | string | Match key 2. Short sort/lookup code |
Word | string | Match key 3. The keyword text |
Supports Fields and DetailedResponse=Y.
Keyword uniques are what you send as KeywordUnique on an invoice line — see Invoices.


