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

VerbPathDelphi methodEnvelope
GET/Invoice/Invoice_HandshakeInvoice_Handshakead hoc
GET/Invoice/Invoices/{InvoiceUnique}InvoicesAPIResponse + Invoice
GET/Invoice/Invoices2/{InvoiceUnique}Invoices2APIResponse + Invoice2
GET/Invoice/InvoiceCountByDateInvoiceCountByDatead hoc
GET/Invoice/InvoicesByDateInvoicesByDateAPIResponse + Invoice
POST/Invoice/addInvoiceupdateaddInvoiceAPIResponse + 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" }
    }
  ]
}
  • Invoice is an array. Multiple invoices per call are processed one at a time; see The Upsert Model for the partial-failure rules.
  • ConnectionInfo is required in practice. Omitting it fails the whole call with Connection Info missing before any invoice is looked at.
  • If Invoice is missing, empty, or not an array, the response is No Invoice records received and 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:

  1. InvoiceHeader.InvoiceUnique (when > 0) — direct unique lookup.
  2. InvoiceHeader.InvoiceNumber (when non-empty) — lookup by invoice number.
  3. InvoiceHeader.ReferenceNo (when non-empty) — lookup by reference number.

A hit turns the call into an update of that invoice. No hit means insert.

ReferenceNo is your idempotency key. Put your own order id there and a repeated post updates the same invoice instead of duplicating it. Conversely, reusing a ReferenceNo across 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 InvoiceType is "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, both InvoiceSubTotal and InvoiceTaxTotal must be present in the header. If either is missing the check fails.
  • Expected total = InvoiceSubTotal + InvoiceTaxTotal.
  • Tendered total = the sum of Amount over the InvoiceTenders entries whose Type maps 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 InvoiceTenders is absent or is not an array, the check is skipped and passes. An unpaid type-C invoice 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 fieldEffect
InvoiceNumberWritten 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.
InvoiceOrderedDate ordered
InvoiceDateInvoice date
InvoiceTypeFirst character only. "Cash" is read as C.
InvoiceSubTypeFirst character only, stored as the tag type
InvoiceDepartmentDepartment the invoice is booked to
InvoiceBookMonthBooking month
InvoiceCustomerCustomer account unique (see step 4)
InvoiceSalesmanSalesperson unique
ReferenceNoReference number — also the match key
InvoiceCommentStored as an attached text blob, not an invoice field

Behaviour worth knowing:

  • InvoiceType and InvoiceSubType take 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 as An error occurred while adding Header information — while the insert may still report success.
  • InvoiceComment is written as a UTF-8 text blob attached to the invoice (file File_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').
  • StockWarning is set to AllowOversell for 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 that

The search:

  1. Candidate accounts are fetched by business name (AName). If you did not supply AName but did supply LastName, a business name is derived from FirstName + LastName (CustomerNameToBusiness).
  2. 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.
  3. 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.
  4. Crucially, each of those five comparisons only happens if the field is present on the candidate record. A stored account with a blank City will not fail the city comparison. This makes the match looser than it looks.
  5. 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 InvoiceBilling without InvoiceCustomer will 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 send InvoiceCustomer and treat InvoiceBilling as 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 fieldEffect
PartUniqueThe inventory part unique. Required in practice.
OrderedSets both the ordered quantity and the shipped quantity
PriceThe inventory price for the line
DescriptionOverrides the part description
KeywordUniqueAttaches a keyword when > 0
LineCommentAttached as a line comment record

Behaviour worth knowing:

  • Ordered drives quantity. There is no separate quantity field; the value is assigned to Line_Ordered and then to Line_Quantity. You cannot order 10 and ship 3 through this endpoint.
  • Ordered is an integer in the model. Fractional quantities are not supported here.
  • An out-of-stock condition raises ES5OutOfStockWarning, which is caught and routed to DoStockWarning with no dialog — the line is accepted. See the oversell note in step 3.
  • Price sets the inventory-type price component. Omitting it lets System Five price the line from the part's own pricing rules.
  • LineComment is 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 information but 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 Amount is a non-empty string. A tender with a disabled type is silently dropped.
  • The invoice's Paid total 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 Amount is an empty string will raise a conversion error while computing the paid total, caught as An error occurred while adding Tender information. Omit the tender rather than sending an empty amount.
  • Use GET /TServerMethodsWebAPI/Get_Tender_Types to 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

ParameterEffect
DetailedResponse=YReturns 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.

  • InvoiceUnique must be greater than zero. Zero or negative returns a success-shaped response with RecordCount: "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: false with 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:

  • InvoiceBilling and InvoiceShipping are 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.
  • InvoiceKeywords changes 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 InvoiceSubTotal and InvoiceTaxTotal as 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=InvoiceHeader returns {"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 Fields returns 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 parameterRequiredNotes
EffectiveDateTimeyesISO 8601, YYYY-MM-DDTHH:MM:SS, no offset, server local time
PageSizenoSee below
PageNumberno1-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, not APIResponse. This endpoint can return either shape depending on where it fails.
  • Pagination is all-or-nothing. If either PageSize < 1 or PageNumber < 1, the unpaginated fetch is used and every matching invoice is returned. Sending PageSize alone 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 nameTypeRequired on inputNotes
InvoiceUniqueintegernoMatch key 1. 0 for new
InvoiceNumberstringnoMatch key 2. Empty -> ASSIGN
ReferenceNostringnoMatch key 3. Recommended idempotency key
InvoiceTypestringyes for totals checkFirst char only. C triggers total/tender validation
InvoiceSubTypestringnoFirst char only
InvoiceDateISO 8601no
InvoiceOrderedISO 8601no
InvoiceBookMonthISO 8601noConverted to a booking month
InvoiceDepartmentintegernoBooking department
InvoiceCustomerintegernoSkips billing auto-match when > 0
InvoiceShipTointegernoPresent in the model; shipping is resolved from InvoiceShipping
InvoiceSalesmanintegerno
InvoiceSubTotalnumberrequired for type CValidation only, never stored
InvoiceTaxTotalnumberrequired for type CValidation only, never stored
InvoiceCommentstringnoStored as an attached text blob

Field reference — InvoiceLines[]

JSON nameTypeNotes
PartUniqueintegerInventory part unique
OrderedintegerSets ordered and shipped quantity
PricenumberInventory price component
DescriptionstringOverrides the part description
KeywordUniqueintegerApplied when > 0
LineCommentstringWritten to a line comment record; failures are silent

Field reference — InvoiceTenders[]

JSON nameTypeNotes
TypestringFirst char only; must be an enabled tender type
AmountnumberNever send an empty string

Field reference — InvoiceBilling / InvoiceShipping

JSON nameUsed for name matchUsed for detail match
ANameyes (primary)
FirstName, LastNamederive AName when AName is blank
Addressyes
Cityyes
StateProvinceyes
Countryyes
ZipPostalyes
Address2, Phone1, Phone2, Fax, AcctNumber, Emailstored on a newly created account only