Source of truth: S5WebAPIServerContainer.pas (DSAuthenticationManager1UserAuthenticate), S5WebAPIServerContainer.dfm (DSAuthenticationManager1), the [TRoleAuth(...)] attributes on each ServerMethods* class, and OpenAPISpecification.AddSecurityDefinitions.
1. Scheme
The API uses HTTP Basic authentication, on every request, to every endpoint. There is no token, no API key, no login endpoint and no session cookie you can reuse. The generated Swagger declares exactly one security definition:
"securityDefinitions": { "basic": { "type": "basic" } }
Send the standard header on every call:
Authorization: Basic base64(username + ":" + password)
Because every server class is registered with LifeCycle = 'Invocation', the credentials are re-evaluated on every request. There is nothing to cache server-side; cache the encoded header on your side instead.
Important: Basic credentials are only as protected as the transport. Use the HTTPS listener in production. The HTTP listener is enabled by default and does not redirect.
2. What the username and password are
The username is a System Five user (dm2.User), not a separate API account. The service looks the user up by the User field of the System Five user file.
The password check accepts two different forms, and either one succeeds:
- The user's System Five password, validated through
dm2.User.TestPassword(Password, 0, 'WebAPI'). This is the normal case: the plain password the user would type into System Five. - A KEK-encrypted hex string. If the supplied password looks like hex (
lib.LooksLikeHex), the service attemptsdm2.Words.KEKDecrypt(Password)and compares the result against the stored password hash (dm2.User.pwHash). This lets an integrator store an encrypted credential rather than the plaintext password.
If the hex decrypt throws, the comparison value is replaced with a random string so the check simply fails rather than erroring.
3. The checks, in order
DSAuthenticationManager1UserAuthenticate runs these in sequence. The first one that fails ends the request.
| # | Check | Failure message (returned to you) |
|---|---|---|
| 1 | Username is non-empty | (no message; request is rejected) |
| 2 | Username is not the literal HealthCheck (see below) | n/a — short-circuits to success |
| 3 | Pervasive/Zen database licence check passes | see note in section 6 |
| 4 | User record exists (dm2.User.GetEqual) | User not found {user}; status {n} |
| 5 | User is not flagged deleted | User has been marked as deleted {user} [{unique}] |
| 6 | User is not locked out | User has been locked out {user} [{unique}] |
| 7 | Password matches (either form above) | Password check failed for user {user} [{unique}] |
| 8 | User has access to the service's department | User and password are okay; but user does not have access to department |
On success the service sets System5.CurrentUser to that user's unique and grants the WebAPI role. Everything the request then writes is attributed to that System Five user — invoice creation, comments, blob records and audit trails all record this user. Give each integration its own System Five user so the audit trail is meaningful.
Departments
Check 8 uses dm2.User.DepartmentAllow(fDepartment), where fDepartment is the service-wide Department value from [System Five Database] in S5WebAPISvc.ini. It is not a per-request parameter and it is not taken from the request body. A user who is valid in System Five but has no access to the department this service instance is configured for cannot use the API at all.
Note that individual endpoints may accept a Department value in their payload (for example an invoice header). That value selects the department the record is written to; it does not change the department your credentials are authorised against.
4. Failure response
Authentication failure is the one place in the API where the HTTP status code is meaningful. The handler sets the DataSnap invocation metadata directly:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"Error":"Password check failed for user WEBUSER [42]"}
The shape is always a single-key object named Error. This is not the Response/Reason envelope used by successful and business-level-failed calls (see Response Envelopes and Error Semantics), so your client needs to handle both shapes.
Every other outcome — including business validation failures — comes back as HTTP 200 with failure indicated inside the body.
5. Roles and which classes they protect
Two roles exist.
| Role | Granted to | Protects |
|---|---|---|
WebAPI | Any successfully authenticated System Five user | Everything except the health check |
HealthCheck | The literal username HealthCheck | HealthCheck class only |
Every server-methods class is decorated [TRoleAuth('WebAPI')] except HealthCheck, which is [TRoleAuth('HealthCheck')].
The HealthCheck bypass
If the supplied username is exactly HealthCheck (case-sensitive), the handler logs a success, grants the HealthCheck role and returns immediately — before any database, user, password or department check. The password is not examined.
This means:
GET /Windward/WebAPI/HealthCheck/Health_Checksucceeds withAuthorization: Basicfor userHealthCheckand any password.- That credential is useless for anything else: the
HealthCheckrole is only authorised for theHealthCheckclass.
Use it for load-balancer and uptime probes; do not use it as a connectivity test for the rest of the API, because it never touches the database and will keep answering while the dataset is unreachable. To prove the database is live, call GET /Windward/WebAPI/TServerMethodsWebAPI/Connect or any *_Handshake operation with real credentials.
A note on the declarative role list
DSAuthenticationManager1.Roles in the .dfm lists the classes the WebAPI role applies to: Inventory, Invoice, Keyword, Units, VirtualInventory, APBill, Category, TServerMethodsWebAPI. Vendors and Customer are absent from that list, although both classes carry [TRoleAuth('WebAPI')] in code. The attribute is what enforces the role at the class level, so both classes still require an authenticated WebAPI user; the omission from the declarative list is an inconsistency, not an open door. Treat every class other than HealthCheck as requiring full credentials.
6. Behaviour when the database is unavailable
Before validating the user, the handler calls TServerMethodsBase.CheckPervasive, which performs a Pervasive/Zen licence and connectivity probe. Known outcomes are reported as:
| Condition | Message |
|---|---|
| Not yet checked | The Pervasive database check has not been done |
| Passed | The Pervasive database check has passed |
| Status 161 | The Pervasive database license check has failed; has your license expired or have you exceeded the maximum number of users? |
| Status 161, demo build | The Pervasive database license check has failed; has your trial version expired? |
| Any other failure | The Pervasive database license check has failed with an unknown error. |
If this probe fails the user-record validation branch is skipped entirely. Your request will not be rejected at the authentication layer, but the endpoint itself will then fail to reach the database and return a business-level failure in the body. Practically: a 403 means bad credentials; a 200 with a failure Response may mean either bad data or an unavailable dataset. Endpoints that expose ValidateDB / Connect are the reliable way to distinguish the two.
7. ConnectionInfo in request bodies is not authentication
Many POST payloads accept a ConnectionInfo object:
{ "ConnectionInfo": { "TerminalNumber": 1 } }
It is easy to mistake this for credentials. It is not.
- The only field read from it is
TerminalNumber(TServerMethodsBase.GetConnectionInfo). Username/password fields are not parsed — the code that read them is commented out. TServerMethodsBase.AuthorizeUser, which gates every POST that goes through the shared upsert pipeline, returnstrueas long as aConnectionInfoobject was supplied and an authenticator is wired up. It does not re-validate credentials.- If
ConnectionInfois missing or malformed,GetConnectionInforeturnsnilandAuthorizeUserfails the whole request withResponse: "Connection Info missing"and no records processed.
So: ConnectionInfo is effectively a required, mostly-inert envelope field on the POST endpoints that use the shared pipeline. Always send it. A safe default is {"ConnectionInfo":{"TerminalNumber":1}}. Real authentication is the Basic header and nothing else.


