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:

  1. 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.
  2. A KEK-encrypted hex string. If the supplied password looks like hex (lib.LooksLikeHex), the service attempts dm2.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.

#CheckFailure message (returned to you)
1Username is non-empty(no message; request is rejected)
2Username is not the literal HealthCheck (see below)n/a — short-circuits to success
3Pervasive/Zen database licence check passessee note in section 6
4User record exists (dm2.User.GetEqual)User not found {user}; status {n}
5User is not flagged deletedUser has been marked as deleted {user} [{unique}]
6User is not locked outUser has been locked out {user} [{unique}]
7Password matches (either form above)Password check failed for user {user} [{unique}]
8User has access to the service's departmentUser 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.

RoleGranted toProtects
WebAPIAny successfully authenticated System Five userEverything except the health check
HealthCheckThe literal username HealthCheckHealthCheck 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_Check succeeds with Authorization: Basic for user HealthCheck and any password.
  • That credential is useless for anything else: the HealthCheck role is only authorised for the HealthCheck class.

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:

ConditionMessage
Not yet checkedThe Pervasive database check has not been done
PassedThe Pervasive database check has passed
Status 161The Pervasive database license check has failed; has your license expired or have you exceeded the maximum number of users?
Status 161, demo buildThe Pervasive database license check has failed; has your trial version expired?
Any other failureThe 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, returns true as long as a ConnectionInfo object was supplied and an authenticator is wired up. It does not re-validate credentials.
  • If ConnectionInfo is missing or malformed, GetConnectionInfo returns nil and AuthorizeUser fails the whole request with Response: "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.