URL class: Invoice — /Windward/WebAPI/Invoice/...
Source of truth: ServerMethodsInvoice.pas, CM_FullInvoice.pas, AO_FullInvoice.pas, plus FullInvoice.pas / ARTran.pas in prgs.
Related legacy operations on TServerMethodsWebAPI (Insert_Full_Invoice, Invoices_Get, InvoiceLines_*, Change_Invoice_Type, Delete_Full_Invoice) are covered in The Legacy TServerMethodsWebAPI Surface.
Operations
| Verb | Path | Delphi method | Envelope |
|---|---|---|---|
| GET | /Invoice/Invoice_Handshake | Invoice_Handshake | ad hoc |
| GET | /Invoice/Invoices/{InvoiceUnique} | Invoices | APIResponse + Invoice |
| GET | /Invoice/Invoices2/{InvoiceUnique} | Invoices2 | APIResponse + Invoice2 |
| GET | /Invoice/InvoiceCountByDate | InvoiceCountByDate | ad hoc |
| GET | /Invoice/InvoicesByDate | InvoicesByDate | APIResponse + Invoice |
| POST | /Invoice/addInvoice | updateaddInvoice | APIResponse + Invoice (ActionResults) |
POST /Invoice/addInvoice
The endpoint the rest of this page exists for. Swagger gives you the payload model; everything below is the behaviour that model implies.
Request shape
{
"ConnectionInfo": { "TerminalNumber": 1 },
"Invoice": [
{
"InvoiceHeader": {
"InvoiceUnique": 0,
"InvoiceNumber": "",
"ReferenceNo": "WEB-100234",
"InvoiceType": "C",
"InvoiceSubType": "",
"InvoiceDate": "2026-09-01T00:00:00",
"InvoiceOrdered": "2026-09-01T00:00:00",
"InvoiceBookMonth":"2026-09-01T00:00:00",
"InvoiceDepartment": 0,
"InvoiceCustomer": 0,
"InvoiceShipTo": 0,
"InvoiceSalesman": 0,
"InvoiceSubTotal": 100.00,
"InvoiceTaxTotal": 12.00,
"InvoiceComment": "Placed via web store"
},
"InvoiceLines": [
{ "PartUnique": 4471, "Ordered": 2, "Price": 50.00,
"Description": "Widget", "KeywordUnique": 0, "LineComment": "" }
],
"InvoiceTenders": [
{ "Type": "C", "Amount": 112.00 }
],
"InvoiceBilling": { "AName": "Acme Ltd", "FirstName": "Jo", "LastName": "Bloggs",
"Address": "1 High St", "Address2": "", "City": "Vernon",
"StateProvince": "BC", "Country": "Canada", "ZipPostal": "V1T 1A1",
"Phone1": "", "Phone2": "", "Fax": "",
"AcctNumber": "", "Email": "jo@example.com" },
"InvoiceShipping": { "AName": "Acme Ltd", "...": "same fields as InvoiceBilling" }
}
]
}Invoiceis an array. Multiple invoices per call are processed one at a time; see The Upsert Model for the partial-failure rules.ConnectionInfois required in practice. Omitting it fails the whole call withConnection Info missingbefore any invoice is looked at.- If
Invoiceis missing, empty, or not an array, the response isNo Invoice records receivedand nothing happens.
Step 1 — Insert or update? (FindRecordMatch)
Before anything else, the server tries to find an existing invoice, in this order, stopping at the first hit:
InvoiceHeader.InvoiceUnique(when> 0) — direct unique lookup.InvoiceHeader.InvoiceNumber(when non-empty) — lookup by invoice number.InvoiceHeader.ReferenceNo(when non-empty) — lookup by reference number.
A hit turns the call into an update of that invoice. No hit means insert.
ReferenceNois your idempotency key. Put your own order id there and a repeated post updates the same invoice instead of duplicating it. Conversely, reusing aReferenceNoacross genuinely different orders will silently overwrite the earlier invoice.
Step 2 — Total and tender reconciliation (CheckTotalAndTenders)
This gate runs before any write. If it fails, nothing is created and the record's ActionResult is:
An error occurred while checking the invoice total and amount paid
The rules:
- The check only applies when
InvoiceTypeis"C". For any other invoice type the check is skipped entirely and totals are never validated."C"is the completed/cash sale type — the case where the money must balance. - For type
C, bothInvoiceSubTotalandInvoiceTaxTotalmust be present in the header. If either is missing the check fails. - Expected total =
InvoiceSubTotal + InvoiceTaxTotal. - Tendered total = the sum of
Amountover theInvoiceTendersentries whoseTypemaps to a tender that is enabled in System Five. Tenders whose type is disabled are excluded from the sum. - The two must match within one cent in either direction (
AccountForPenny). This tolerance exists for rounding; do not rely on it for anything larger. - If
InvoiceTendersis absent or is not an array, the check is skipped and passes. An unpaid type-Cinvoice is therefore accepted.
InvoiceSubTotal and InvoiceTaxTotal are validation-only. They are not written to the invoice — System Five recalculates the real totals from the lines and the tax area. Do not treat them as a way to force a total.
Step 3 — Header (ProcessHeaderInfo)
Fields are copied from InvoiceHeader onto the invoice. Every field is optional; an absent field is simply not set.
| JSON field | Effect |
|---|---|
InvoiceNumber | Written as the invoice number. Empty or absent becomes the literal ASSIGN, which tells System Five to allocate the next number. This is the normal case. |
InvoiceOrdered | Date ordered |
InvoiceDate | Invoice date |
InvoiceType | First character only. "Cash" is read as C. |
InvoiceSubType | First character only, stored as the tag type |
InvoiceDepartment | Department the invoice is booked to |
InvoiceBookMonth | Booking month |
InvoiceCustomer | Customer account unique (see step 4) |
InvoiceSalesman | Salesperson unique |
ReferenceNo | Reference number — also the match key |
InvoiceComment | Stored as an attached text blob, not an invoice field |
Behaviour worth knowing:
InvoiceTypeandInvoiceSubTypetake only the first character of whatever you send. Sending a word does not error; it silently uses its initial. Sending an empty string will raise an exception inside the header handler (InvoiceType[1]on an empty string), which is caught and recorded asAn error occurred while adding Header information— while the insert may still report success.InvoiceCommentis written as a UTF-8 text blob attached to the invoice (fileFile_Invoice), created under the authenticated user's id and dated today. It is a single line: the whole string is added as one comment line.- After the header is applied the invoice is saved, reloaded, and given a temporary number via
MakeInvoiceNumber('C'). StockWarningis set toAllowOversellfor the whole operation. Lines that would drive stock negative are accepted silently — no warning, no error. If you need to prevent overselling, check availability before posting.
Step 4 — Billing: the customer-matching and auto-creation rules
This is the behaviour most likely to surprise you, and it is not in Swagger.
if InvoiceHeader.InvoiceCustomer > 0:
use that account, done.
else if InvoiceBilling is present:
search for a matching account
if found -> use it
if not found -> CREATE A NEW CUSTOMER ACCOUNT and use thatThe search:
- Candidate accounts are fetched by business name (
AName). If you did not supplyANamebut did supplyLastName, a business name is derived fromFirstName+LastName(CustomerNameToBusiness). - Each candidate's name is compared to yours with all spaces removed and case ignored.
"Acme Ltd","ACMELTD"and"acme ltd"are all the same name to this matcher. - A surviving candidate must then match on address details (
MatchAccountRecordByDetail):Address,City,StateProvince,Country,ZipPostal— each compared with the same spaces-removed / case-insensitive rule. - Crucially, each of those five comparisons only happens if the field is present on the candidate record. A stored account with a blank
Citywill not fail the city comparison. This makes the match looser than it looks. - The first candidate that passes wins. There is no scoring and no ambiguity report.
If nothing matches, a new account is inserted (AddAccountByDetails) and the invoice is attached to it.
Consequence: an integration that posts
InvoiceBillingwithoutInvoiceCustomerwill steadily create new customer accounts every time an address detail differs by more than whitespace — a different apartment format, a typo, a changed postcode. The recommended pattern is to resolve the customer yourself (via the Customer endpoints), then sendInvoiceCustomerand treatInvoiceBillingas descriptive only.
Step 5 — Shipping
ProcessShipping applies exactly the same algorithm to InvoiceShipping, resolving or creating a ship-to account, then refreshing the invoice's ship-to. The same auto-creation caveat applies.
Step 6 — Lines (ProcessLines)
Before any line is added, the invoice's tax area is recalculated from the resolved bill-to/ship-to (FindTaxArea). This is why billing and shipping are processed before lines: the tax outcome depends on them.
Per line entry:
| JSON field | Effect |
|---|---|
PartUnique | The inventory part unique. Required in practice. |
Ordered | Sets both the ordered quantity and the shipped quantity |
Price | The inventory price for the line |
Description | Overrides the part description |
KeywordUnique | Attaches a keyword when > 0 |
LineComment | Attached as a line comment record |
Behaviour worth knowing:
Ordereddrives quantity. There is no separate quantity field; the value is assigned toLine_Orderedand then toLine_Quantity. You cannot order 10 and ship 3 through this endpoint.Orderedis an integer in the model. Fractional quantities are not supported here.- An out-of-stock condition raises
ES5OutOfStockWarning, which is caught and routed toDoStockWarningwith no dialog — the line is accepted. See the oversell note in step 3. Pricesets the inventory-type price component. Omitting it lets System Five price the line from the part's own pricing rules.LineCommentis written to a separate comment record keyed by the line's unique. Failures here are swallowed silently — the comment simply will not exist, with no message anywhere in the response.- Every line triggers a save of the line and of the invoice, so a long invoice is many round trips to the database. Combined with the global lock, very large invoices are the main cause of API stalls.
- Any exception in this block records
An error occurred while adding Line informationbut does not fail the record.
Step 7 — Tenders (ProcessTenders)
Per tender entry:
Type— first character only, matched against the System Five tender list.Amount— parsed as a number.
Rules and known quirks:
- A tender is added to the invoice's tender list only when its type is enabled in System Five and
Amountis a non-empty string. A tender with a disabled type is silently dropped. - The invoice's
Paidtotal is incremented for every entry regardless of whether the tender itself was added. The enabled/non-empty test guards only the tender-list insertion, not the running paid total. A payload containing a disabled tender type therefore records money as paid without a matching tender line. Send only enabled tender types. - An entry whose
Amountis an empty string will raise a conversion error while computing the paid total, caught asAn error occurred while adding Tender information. Omit the tender rather than sending an empty amount. - Use
GET /TServerMethodsWebAPI/Get_Tender_Typesto discover the enabled tender types for the dataset before posting.
Step 8 — Commit
Insert path order: header -> save/reload -> billing -> shipping -> lines -> tenders -> MakeInvoiceNumber('M') -> Save -> Post.
Update path order: load by unique -> header -> billing -> shipping -> lines -> tenders -> Save -> Post.
Note that on the update path the existing lines are not cleared first. Lines in your payload are appended to the invoice. To correct an invoice's lines you must handle removal separately; re-posting the same invoice with the same lines will double them.
ActionSuccess reflects only the final Btrieve status. On success:
Inserted Record, subject to System 5 verification Updated Record, subject to System 5 verification
RecordId is the invoice unique; RecordDesc is the assigned invoice number.
Query parameters
| Parameter | Effect |
|---|---|
DetailedResponse=Y | Returns the full invoice in each RecordResult |
Fields=... | Shapes RecordResult, and implies DetailedResponse=Y |
Use DetailedResponse=Y on creation — it is the cheapest way to learn the assigned invoice number and the totals System Five actually computed.
GET /Invoice/Invoices/{InvoiceUnique}
Returns exactly one invoice. {InvoiceUnique} is a path segment.
InvoiceUniquemust be greater than zero. Zero or negative returns a success-shaped response withRecordCount: "0"and the message:This Invoice Unique value is invalid: {n}\rThe value must be greater than ZERO!- A valid but non-existent unique returns
IsSuccess: falsewith an empty array (see Response Envelopes and Error Semantics — "not found" and "failed" are the same flag).
Response shape
The array key is Invoice. Each entry has five sections:
{
"InvoiceHeader": { "InvoiceUnique": 10231, "InvoiceSubTotal": 100.0,
"InvoiceTaxTotal": 12.0, "InvoiceNumber": "INV-000451",
"InvoiceDate": "...", "InvoiceType": "C",
"InvoiceDepartment": 0, "InvoiceCustomer": 88,
"InvoiceShipTo": 88, "InvoiceSalesman": 3,
"ReferenceNo": "WEB-100234", "InvoiceComment": "..." },
"InvoiceLines": [ { "PartUnique": 4471, "Price": 50.0, "Ordered": 2,
"Description": "Widget", "InvoiceKeywords": "",
"LineComment": "" } ],
"InvoiceTenders": [ { "Type": "C", "Amount": 112.0 } ],
"InvoiceBilling": [ { "...account record..." : "..." } ],
"InvoiceShipping": [ { "...account record..." : "..." } ]
}Points that catch people out:
InvoiceBillingandInvoiceShippingare arrays containing at most one account object, even though only one is ever possible. They are empty arrays when the invoice has no bill-to / ship-to.InvoiceKeywordschanges type. When the line has a keyword it is an object ({Unique, Sort, Word}); when it does not, it is an empty string. Handle both.- Layer lines are omitted. Only non-layer lines appear. An invoice built from kits or layered items will not round-trip through this shape.
- The header carries
InvoiceSubTotalandInvoiceTaxTotalas System Five computed them — unlike on input, where they are validation-only.
Fields on the GET is two-level and easy to get wrong
Fields is checked both for the five section names and for every individual field name inside those sections.
?Fields=InvoiceHeaderreturns{"InvoiceHeader": {}}— the section is allowed but none of its inner fields are.- To get a useful subset you must list the section and the fields:
?Fields=InvoiceHeader,InvoiceNumber,InvoiceCustomer,ReferenceNo - Omitting
Fieldsreturns everything, which is almost always what you want.
GET /Invoice/Invoices2/{InvoiceUnique}
Identical to Invoices except that the billing and shipping sections are rendered in the same format as the addInvoice input model (InvoiceAddress rather than the fuller account record). The array key is Invoice2.
Use Invoices2 when you want to read an invoice, change it and post it back: the addresses round-trip. Use Invoices when you want the full account detail.
GET /Invoice/InvoicesByDate
Returns every invoice created on or after a date.
| Query parameter | Required | Notes |
|---|---|---|
EffectiveDateTime | yes | ISO 8601, YYYY-MM-DDTHH:MM:SS, no offset, server local time |
PageSize | no | See below |
PageNumber | no | 1-based |
Behaviour:
- A date that fails to parse returns immediately:
{"Response":"Failed","Reason":"Invalid Start Date found (...); must be in ISO8601 format"}— note this is the legacy envelope, notAPIResponse. This endpoint can return either shape depending on where it fails. - Pagination is all-or-nothing. If either
PageSize < 1orPageNumber < 1, the unpaginated fetch is used and every matching invoice is returned. SendingPageSizealone does not paginate. - With no matches you get the legacy envelope again:
{"Response":"Success","Reason":"No Invoice records were found on, or after, Start Date: (...)"}. - Each invoice is fetched and rendered individually, at full detail — this is an expensive call. The Swagger text warns about memory errors for good reason.
- The service holds its global lock for the entire call.
Use InvoiceCountByDate first
GET /Invoice/InvoiceCountByDate?EffectiveDateTime=2026-09-01T00:00:00
Returns the ad hoc shape:
{ "Invoice Count by Date": "Invoice", "Record Count": "1743" }or, when there are none:
{ "Response": "Success",
"Reason": "No Invoice records were found on, or after, Start Date: (...)" }Note the odd key names — Invoice Count by Date and Record Count both contain spaces, and Record Count is a string. The intended pattern is: call this, divide by your page size, then loop InvoicesByDate with both PageSize and PageNumber. Remember the count and the pages are not taken atomically.
GET /Invoice/Invoice_Handshake
{ "System Five Invoice API": "Handshake", "Version": "1.2.3.4" }Confirms the class is reachable and reports the service build. Requires valid credentials, so it is also a cheap credential test — unlike the health check, which authenticates nobody.
Field reference — InvoiceHeader
| JSON name | Type | Required on input | Notes |
|---|---|---|---|
InvoiceUnique | integer | no | Match key 1. 0 for new |
InvoiceNumber | string | no | Match key 2. Empty -> ASSIGN |
ReferenceNo | string | no | Match key 3. Recommended idempotency key |
InvoiceType | string | yes for totals check | First char only. C triggers total/tender validation |
InvoiceSubType | string | no | First char only |
InvoiceDate | ISO 8601 | no | |
InvoiceOrdered | ISO 8601 | no | |
InvoiceBookMonth | ISO 8601 | no | Converted to a booking month |
InvoiceDepartment | integer | no | Booking department |
InvoiceCustomer | integer | no | Skips billing auto-match when > 0 |
InvoiceShipTo | integer | no | Present in the model; shipping is resolved from InvoiceShipping |
InvoiceSalesman | integer | no | |
InvoiceSubTotal | number | required for type C | Validation only, never stored |
InvoiceTaxTotal | number | required for type C | Validation only, never stored |
InvoiceComment | string | no | Stored as an attached text blob |
Field reference — InvoiceLines[]
| JSON name | Type | Notes |
|---|---|---|
PartUnique | integer | Inventory part unique |
Ordered | integer | Sets ordered and shipped quantity |
Price | number | Inventory price component |
Description | string | Overrides the part description |
KeywordUnique | integer | Applied when > 0 |
LineComment | string | Written to a line comment record; failures are silent |
Field reference — InvoiceTenders[]
| JSON name | Type | Notes |
|---|---|---|
Type | string | First char only; must be an enabled tender type |
Amount | number | Never send an empty string |
Field reference — InvoiceBilling / InvoiceShipping
| JSON name | Used for name match | Used for detail match |
|---|---|---|
AName | yes (primary) | |
FirstName, LastName | derive AName when AName is blank | |
Address | yes | |
City | yes | |
StateProvince | yes | |
Country | yes | |
ZipPostal | yes | |
Address2, Phone1, Phone2, Fax, AcctNumber, Email | stored on a newly created account only |


