URL class: Customer — /Windward/WebAPI/Customer/...

Source of truth: ServerMethodsCustomer.pas, CM_Customer.pas, CM_Account.pas, CM_Contact.pas, AO_Customer.pas.

The older customer operations on TServerMethodsWebAPI (Get_Customers, List_Customers, Get_Customer_By_Email, Insert_Retail_Customer, Update_Retail_Customer, Customers_Insert/Read/Update) are covered in The Legacy TServerMethodsWebAPI Surface. Prefer this class for new work.


Operations

VerbPathDelphi methodEnvelope
GET/Customer/Customer_HandshakeCustomer_Handshakead hoc
GET/Customer/GetCustomerRecordCountGetCustomerRecordCountad hoc
GET/Customer/Customers/{Unique}CustomersAPIResponse + Customer
GET/Customer/CustomerChangesCustomerChangesAPIResponse + Customer
POST/Customer/addCustomerupdateaddCustomerAPIResponse + Customer

What "customer" means here

A System Five account record carries a type. This class only deals with accounts of type R (retail customer). The same underlying file holds vendors (type P) and other account kinds, which is why several operations check the type and refuse to act on the wrong one.

Contacts are child records of a customer and are managed through the Contacts array inside the customer payload — there is no standalone contact endpoint.


POST /Customer/addCustomer

An upsert. See The Upsert Model for the shared rules.

Request shape

{
  "ConnectionInfo": { "TerminalNumber": 1 },
  "Customers": [
    {
      "Unique": 0,
      "Name": "Bloggs, Jo",
      "FirstName": "Jo",
      "LastName": "Bloggs",
      "Addr1": "1 High St",
      "City": "Vernon",
      "State": "BC",
      "Country": "Canada",
      "Postal": "V1T 1A1",
      "Email": "jo@example.com",
      "Department": 0,
      "Contacts": [ { "FirstName": "Sam", "LastName": "Smith" } ]
    }
  ]
}

Match rule — and why this endpoint duplicates easily

The only match attempt is Unique (key 0), and the matched account must have type R.

  • No Unique, or Unique of 0 -> always an insert. There is no name, email or phone based matching.
  • A Unique that exists but is not a customer is skipped: Unique provided exists, but does not correspond to a Customer record. Customer record skipped

There is no natural key for customers. Posting the same person twice creates two accounts. Capture RecordId from the response on first creation and store it on your side; send it back as Unique on every subsequent write. If you need to find an existing customer before deciding, use POST /TServerMethodsWebAPI/Get_Customer_By_Email or Get_Customers with filters (see The Legacy TServerMethodsWebAPI Surface).

The name gate

Before anything is written, the server derives a business name:

  1. If Name is present and non-empty, use it.
  2. Otherwise, if both FirstName and LastName are present and non-empty, build the name from them.
  3. Otherwise the record is rejected with:
An error occurred while adding the Name or First and Last Names passed. Check your parameters.

The composition in step 2 depends on a System Five setup question, SQ_BusinessNameFormatToFirstLast:

Setup questionResulting Name
enabledFirstName + " " + LastName -> Jo Bloggs
disabled (default)LastName + ", " + FirstName -> Bloggs, Jo

The same payload therefore produces a different stored name on different datasets. If the exact stored name matters to you, send Name explicitly rather than relying on the derivation.

Insert path: defaults come from a template account

For an insert, the customer is created with CreateNew('R', dept, iDefault, sAName), where iDefault is the id of a default customer record chosen by GetDefaultCustomerRecord(Country, State, Department) using the Country, State and Department from your payload.

This is important and entirely undocumented in Swagger: the new customer inherits terms, tax settings, price schedule and other defaults from a template account selected by country/state/department. Fields you do not send are not blank — they carry the template's values. Sending Country and State changes which template is used, and therefore changes tax and pricing defaults.

After creation, every field you did send is applied over the top.

Field application rules

Each field is applied only when present in the payload; absent fields are left alone (on update, existing values survive). Type coercions to know about:

KindFieldsRule
Single characterDuty, FedTax, AutoDiscount, CSType, TaxStatus, GSTExempt, RetailType, ForeignFirst character only. Sending an empty string raises an exception, caught as An error occured during the update or insert of the record, and fails the record. Omit rather than send "".
DateContractDate, POExpiryDate, LastVisitISO 8601. An unparseable value silently becomes the Pervasive zero date.
Department/currency scopedPOBilledSoFar, BalancesWritten against the account's current Department and CurrencyCode. If you are also changing Department or CurrencyCode in the same payload, the order of application matters — send them in a prior call if you need certainty.
BooleanECommerceJSON boolean
IgnoredWarningComments, LookupWordsPresent in the model but not written — there is no corresponding account property.

Shipping address — a conditional you will trip over

The block that saves ShipAddr1 / ShipCity / ShipState / ShipCountry / ShipPostal / ShipPhone1 only runs when ShipName is present and equal to the empty string:

if TryGetJSONValue(aRecord, aocustShipName, sValue, false) and (sValue = '') then

So:

  • Omitting ShipName entirely -> the shipping address is not saved.
  • Sending "ShipName": "" -> the shipping address is saved.
  • Sending a real ShipName -> the shipping address is not saved.

To store a shipping address through this endpoint you must send "ShipName": "" alongside the address fields. The address is stored as a free-text block (SaveShippingAddress), one line per supplied field in the order addr1, addr2, city, state, country, postal, phone.

Contacts

Contacts is an array of contact objects, processed only when the customer write succeeded and produced a RecordId > 0. Each contact is matched by:

  1. ContactId (key 0) — must already belong to this customer, otherwise skipped with ContactId provided exists, but does not belong to the Customer/Vendor record. Contact record skipped
  2. FirstName + LastName + customer (key 5), compared case-insensitively

Contacts produce no result entries. They go through PutChildRecords, which discards per-record outcomes. A contact can fail silently while the customer reports success. Read the customer back if contacts matter.

Response

{
  "APIResponse": { "IsSuccess": true, "Response": "", "RecordCount": "1" },
  "Customer": [
    { "RecordId": "8842", "RecordDesc": "Bloggs, Jo",
      "ActionResult": "Inserted Record, subject to System 5 verification",
      "ActionSuccess": true, "RecordResult": null }
  ]
}

RecordDesc is the stored business name — useful for confirming which naming convention the dataset applied.

Query parameters

ParameterEffect
DetailedResponse=YPopulate RecordResult with the written customer
Fields=...Shape RecordResult; implies DetailedResponse=Y

GET /Customer/Customers/{Unique}

{Unique}Behaviour
> 0Return that one customer
0Return all customers — paginate or expect a very large response

Query parameters

ParameterNotes
FieldsComma-separated field names, matched against the response model
PageSizeOnly meaningful with {Unique} = 0
PageNumber1-based; required whenever PageSize > 0
FreeFormNameMap=YAdds the FreeFormHeaders map (see Common Parameters)

Pagination is a forward scan — cost grows with page number

GetNextID finds the first record of the requested page by walking the customer index from the beginning, counting records until it passes PageSize * (PageNumber - 1).

  • Page 1 is free; page 100 with PageSize=500 walks ~49 500 records first.
  • The walk happens inside the service's global lock, so deep pages stall the whole API.
  • Requesting a page beyond the end sets no records and adds an informational message to APIResponse.Response:
No records found on page 12

Practical guidance: use modest page counts, or prefer CustomerChanges with an advancing date watermark for ongoing synchronisation.

The unpaginated ceiling

With no pagination the walk stops reporting at cMaxPageSize (50 000) and appends to APIResponse.Response:

Max Page Size of 50000 exceeded!

Note this is a message, not an error — IsSuccess can still be true. If you see this text your result set is truncated.

Response

Array key Customer. Each record carries the account fields listed in the field reference below, plus:

  • Contacts — an array of the customer's contact records
  • FreeFormGroup — free-form field values, by numeric id
  • ShippingAddress — the stored shipping address block

GET /Customer/CustomerChanges

Returns customers changed on or after a date, using System Five's record state tracking.

Query parameterRequiredNotes
EffectiveDateTimeyesISO 8601, server local time
PageSize / PageNumbernoBoth or neither

Prerequisite: record state tracking must be enabled

If it is not, the endpoint returns the legacy envelope immediately and does no work:

{ "Response": "Failed",
  "Reason": "Record State tracking has not been enabled for this database; FetchChanges cannot work without that setting turned on" }

This is a System Five configuration setting, not an API parameter. Every *Changes / FetchChanges endpoint in this API has the same prerequisite. Check it once during onboarding rather than per call.

Other early returns (also legacy envelope)

{ "Response": "Failed", "Reason": "Invalid EffectiveDate found (...); must be in ISO8601 format" }
{ "Response": "Success", "Reason": "No record changes in Customers were found for EffectiveDate: (...)" }

Only when there is at least one change does the response switch to the APIResponse + Customer shape. This endpoint can return either envelope generation — handle both.

What "changed" means

The record-state file records touched records with a timestamp. The endpoint fetches the record ids changed at or after your date, then reads each customer in full. Consequences:

  • You get whole records, not diffs. There is no indication of which fields changed.
  • A record touched but not materially altered still appears.
  • Deletions are not distinguishable from updates through this endpoint — you receive the current state of whatever ids are listed.
  • Pagination here pages the change list, not the customer file, so it does not suffer the forward-scan cost of Customers. Prefer this endpoint for sync.

Recommended sync loop

watermark = last successful run time (server local)
loop:
    GET /Customer/CustomerChanges?EffectiveDateTime={watermark}&PageSize=500&PageNumber={n}
    until fewer than PageSize records come back
watermark = time the run started (not finished)

Use the run start time as the next watermark so records changed during the run are picked up next time. Expect to reprocess a small overlap; the payloads are full records so reprocessing is idempotent on your side.


GET /Customer/GetCustomerRecordCount

{ "SystemFive API Record Count": "Customers", "Record Count": "12043" }

Ad hoc envelope; both keys contain spaces and Record Count is a string.

Note the count is produced by walking every customer record on the name index, not by a stored counter. It is an O(n) call under the global lock. Do not poll it.


GET /Customer/Customer_Handshake

{ "System Five Customer API": "Handshake", "Version": "1.2.3.4" }

Field reference

Input fields for addCustomer; the same names appear in GET responses.

Identity and name

FieldTypeNotes
UniqueintegerMatch key. 0/absent -> insert
NamestringBusiness name. Preferred — avoids the derivation rule
FirstName, LastNamestringUsed to derive Name when Name is blank; both required
NumberstringCustomer number

Address and contact

FieldType
Addr1, Addr2, City, State, Country, Postalstring
Phone1, Phone2, Phone3string
Email, WebCommentsstring

Country and State also select the default template on insert — see above.

Shipping (requires "ShipName": "" — see the conditional above)

ShipName, ShipAddr1, ShipAddr2, ShipCity, ShipState, ShipCountry, ShipPostal, ShipPhone1

Commercial terms

FieldTypeNotes
Termsstring
DueDaysinteger
Interestinteger
CreditLimitnumber
PriceScheduleinteger
ContractDateISO 8601
SalesmanintegerSalesperson unique
DepartmentintegerAlso selects the insert template
CurrencyCodeinteger
BalancesnumberWritten for the account's current department + currency

Discounts

APDiscount (integer), APDiscountRule (integer enum), APDiscountDays (integer), AutoDiscount (single char), DisNv (integer), DisStat (integer)

Tax

FieldTypeNotes
TaxStatussingle char
TaxNumber, TaxCodestring
FedTax, GSTExempt, Duty, Foreignsingle char
TaxBit1 .. TaxBit8present in the model

Purchase orders

StandingPO (string), POExpiryDate (ISO 8601), POMaximumValue (number), POBilledSoFar (number, department + currency scoped)

Banking / EFT

BankInfo, EFTAccount, EFTBank, EFTName (all string)

Other

FieldTypeNotes
ECommerceboolean
PasswordstringWritten to the account password field
RetailType, CSTypesingle char
LastVisitISO 8601
TimeZoneinteger
ShipNointegerMaps to the account's FinancedBy field
SearchContractintegerMaps to SearchContact
ContactsarrayChild records; no result entries
FreeFormGrouparrayFree-form values by id
WarningComments, LookupWordsstringAccepted but never written