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.
| Generation | Top-level keys | Used by |
|---|---|---|
| A — legacy | Response, Reason, Results, Stopwatch | Older TServerMethodsWebAPI operations |
| B — modern | APIResponse, plus one domain-named array | The Invoice, Customer, Inventory, APBill, Vendors, Category, Units, Keyword, VirtualInventory classes |
| C — ad hoc | Whatever the method built | Handshakes, 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"
}| Field | Type | Present when | Meaning |
|---|---|---|---|
Response | string | always | Literally "Success" or "Failure". Nothing else. |
Reason | string | only when non-empty | Human-readable explanation. Free text; do not parse it. |
Results | object | only when the call produced data | The payload. Absent on failure and on empty results. |
Stopwatch | string | only when the endpoint timed itself | Whole 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
| Field | Type | Meaning |
|---|---|---|
IsSuccess | boolean | The authoritative success flag. A real JSON boolean, not a string. |
Response | string | Free-text message. On success it is often a summary such as "1 Invoice received"; on failure it carries the reason. May be empty. |
ElapsedTime | string | Server-side duration. |
RecordCount | string | Number 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 / area | Array key |
|---|---|
Invoice (getInvoice, getInvoicesByDate, addInvoice) | Invoice |
Invoice v2 (GETInvoice2) | Invoice2 |
| Customer | Customer |
| Inventory | Inventory |
| AP bill | APBill |
| Vendor | Vendor |
| Category | Category |
| Unit | Unit |
| Keyword | Keyword |
| Virtual inventory | VirtualInventory |
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
}
]
}| Field | Type | Meaning |
|---|---|---|
RecordId | string | The System Five unique of the record created or updated. "0" when nothing was written. |
RecordDesc | string | A human-facing identifier — for an invoice, the assigned invoice number. |
ActionResult | string | What happened. See the common values below. |
ActionSuccess | boolean | Whether this record succeeded. |
RecordResult | object or null | The 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 verificationUpdated Record, subject to System 5 verificationAn error occurred during the insert of the recordAn 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.IsSuccessis the logical AND of every record'sActionSuccess. A single bad record makes the whole response report failure even though other records were written.
Never infer "nothing was written" from
IsSuccess: falseon a POST. Always walk the record array and act on eachActionSuccess/RecordIdindividually. 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
- Send Basic auth on every request.
- Treat 403 as an auth failure and parse
{"Error": "..."}. - For everything else, ignore the status code.
- Unwrap
resultif present. - Determine success from
APIResponse.IsSuccess(generation B) orResponse == "Success"(generation A). - On POSTs, iterate the record array and record every
RecordId/ActionSuccesspair before deciding whether to retry. - Never retry a partially-succeeded batch blindly.


