Source of truth: AO_Base.pas (APIResponse, APIResults, ActionResult, TActionParams), WebAPI_Base.pas (WebAPIResult, WebAPIResponse), ServerMethodsBase.PopulateResults, CM_Base.PutRecords, S5WebAPIServerContainer.HTTPServiceFormatResult.

Read Transport, URLs and Hosting first — in particular, the HTTP status code is always 200 except for authentication failures. Everything below is about finding the real outcome inside the body.


1. There are three envelope generations

The API grew in layers and never retired the old ones. You will meet all three. Identify which one an endpoint uses from its per-area page; the shapes are distinguishable at runtime by their top-level keys.

GenerationTop-level keysUsed by
A — legacyResponse, Reason, Results, StopwatchOlder TServerMethodsWebAPI operations
B — modernAPIResponse, plus one domain-named arrayThe Invoice, Customer, Inventory, APBill, Vendors, Category, Units, Keyword, VirtualInventory classes
C — ad hocWhatever the method builtHandshakes, counts, health check, a few one-off operations

All three may additionally be wrapped in DataSnap's {"result":[ ... ]} envelope — see section 5.


2. Generation A — the legacy Response / Reason envelope

Built by TServerMethodsBase.PopulateResults:

{
  "Response":  "Success",
  "Reason":    "3 Records found",
  "Results":   { "...": "..." },
  "Stopwatch": "2"
}
FieldTypePresent whenMeaning
ResponsestringalwaysLiterally "Success" or "Failure". Nothing else.
Reasonstringonly when non-emptyHuman-readable explanation. Free text; do not parse it.
Resultsobjectonly when the call produced dataThe payload. Absent on failure and on empty results.
Stopwatchstringonly when the endpoint timed itselfWhole seconds elapsed, as a string.

Reading rule: Response == "Success". Do not test for the presence of Results — a successful call that found nothing omits it entirely.

Note "Failed" (not "Failure") appears in a few hand-rolled failure paths, for example the ISO 8601 date-parsing failures in the Invoice endpoints. Treat anything that is not exactly "Success" as failure rather than matching on the failure spelling.


3. Generation B — the modern APIResponse envelope

This is the one to write new integrations against. It is produced by serialising an APIResults descendant, and looks like:

{
  "APIResponse": {
    "IsSuccess":   true,
    "Response":    "1 Invoice received",
    "ElapsedTime": "0.031",
    "RecordCount": "1"
  },
  "Invoice": [ { "...": "..." } ]
}

APIResponse

FieldTypeMeaning
IsSuccessbooleanThe authoritative success flag. A real JSON boolean, not a string.
ResponsestringFree-text message. On success it is often a summary such as "1 Invoice received"; on failure it carries the reason. May be empty.
ElapsedTimestringServer-side duration.
RecordCountstringNumber of entries in the record array, as a string.

RecordCount is a string, not a number — a recurring source of client-side type errors. So is RecordId inside ActionResult (section 4).

The record array

The array is not called Records. Each result class renames it to a domain-specific key via [TOpenAPIProperty(...)]:

Class / areaArray key
Invoice (getInvoice, getInvoicesByDate, addInvoice)Invoice
Invoice v2 (GETInvoice2)Invoice2
CustomerCustomer
InventoryInventory
AP billAPBill
VendorVendor
CategoryCategory
UnitUnit
KeywordKeyword
Virtual inventoryVirtualInventory

Check the per-area page for the exact key; do not assume it from the URL.

Empty results

GetRecords sets IsSuccess := (RecordList.Count > 0). So a well-formed query that simply matched nothing returns IsSuccess: false, not an empty success. You cannot distinguish "not found" from "failed" on IsSuccess alone — use RecordCount and Response together.


4. POST results: ActionResult, one entry per submitted record

Every POST that goes through the shared upsert pipeline (CM_Base.PutRecords) returns one array entry per record you submitted, in submission order, built by ActionResult.ToJSON:

{
  "APIResponse": { "IsSuccess": false, "Response": "2 Invoice received", "RecordCount": "2" },
  "Invoice": [
    {
      "RecordId":      "10231",
      "RecordDesc":    "INV-000451",
      "ActionResult":  "Inserted Record, subject to System 5 verification",
      "ActionSuccess": true,
      "RecordResult":  null
    },
    {
      "RecordId":      "0",
      "RecordDesc":    "",
      "ActionResult":  "An error occurred while checking the invoice total and amount paid",
      "ActionSuccess": false,
      "RecordResult":  null
    }
  ]
}
FieldTypeMeaning
RecordIdstringThe System Five unique of the record created or updated. "0" when nothing was written.
RecordDescstringA human-facing identifier — for an invoice, the assigned invoice number.
ActionResultstringWhat happened. See the common values below.
ActionSuccessbooleanWhether this record succeeded.
RecordResultobject or nullThe full record as written, only when you asked for a detailed response (see Common Parameters). Otherwise null.

Common ActionResult strings:

  • Inserted Record, subject to System 5 verification
  • Updated Record, subject to System 5 verification
  • An error occurred during the insert of the record
  • An error occurred during the update of the record
  • domain-specific validation messages (see each area page)

The phrase "subject to System 5 verification" is meaningful: it says the write was accepted by the Btrieve layer, not that System Five's own business validation has blessed the resulting document.

Partial success is normal — and is the most important rule on this page

aResults.Response.IsSuccess := aResults.Response.IsSuccess and oActionResult.Success;
  • Records are processed in a loop, one at a time.
  • There is no transaction spanning the batch. Record 1 can be committed and record 2 fail; record 1 stays committed.
  • The top-level APIResponse.IsSuccess is the logical AND of every record's ActionSuccess. A single bad record makes the whole response report failure even though other records were written.

Never infer "nothing was written" from IsSuccess: false on a POST. Always walk the record array and act on each ActionSuccess / RecordId individually. Retrying the whole batch after a partial failure will duplicate the records that succeeded, unless your payload carries the identifiers that let the upsert matcher find them again (see The Upsert Model).

Errors that do not set ActionSuccess: false

Domain controllers wrap their sub-steps in try ... except blocks that record a message into ActionResult but do not clear ActionSuccess. For example, in invoice creation a failure inside line processing sets "An error occurred while adding Line information" while the outer flow can still report the insert as successful.

Defensive rule for clients: treat a record as fully successful only when ActionSuccess is true and ActionResult starts with Inserted Record or Updated Record. Anything containing "An error occurred" deserves a reconciliation read-back regardless of the flag.


5. The DataSnap result wrapper

DataSnap wraps a method's return value:

{ "result": [ { "APIResponse": { "...": "..." } } ] }

Endpoints declared with TOpenAPIMethodAttribute(<verb>, true) are stripped of that wrapper by HTTPServiceFormatResult and return the inner object directly. Most of the modern (generation B) endpoints do this; most handshake and legacy endpoints do not.

Because the two forms coexist, normalise on the client:

if body has exactly one key "result" and body.result is an array:
    body = body.result[0]

This is safe against every endpoint in the service, because no domain envelope uses a top-level key named result.


6. Exceptions

Unhandled exceptions inside an endpoint are caught at the method boundary, logged to the Windows event log / Log Analytics, and surfaced as a 200 with a failure envelope whose message is the raw Delphi exception text, for example:

{ "Response": "Failure", "Reason": "Access violation at address ..." }

There is no error code, no structured error object and no stable message catalogue. Log the whole body when a call fails; do not build control flow on message text.


7. Checklist for a robust client

  1. Send Basic auth on every request.
  2. Treat 403 as an auth failure and parse {"Error": "..."}.
  3. For everything else, ignore the status code.
  4. Unwrap result if present.
  5. Determine success from APIResponse.IsSuccess (generation B) or Response == "Success" (generation A).
  6. On POSTs, iterate the record array and record every RecordId / ActionSuccess pair before deciding whether to retry.
  7. Never retry a partially-succeeded batch blindly.