Kombine Flex Portal API · v1 · Integration guide
Build your own portal or connect an AI agent
The API provides the same information used by the official portal. Send ordinary HTTPS requests and receive JSON. You do not need database access, a particular SDK, or a special agent protocol.
Getting started: Find the API address → log in as a manager → request /api/v1/session/me.
Available now: API availability, the public active-user count, and the signed-in manager’s name, icon, Tabs, bank/location access, and operation permissions. These are the data displayed on the portal’s current overview. Users2 lists are available through GetBankUsers. Other bank, location and unit records and editing operations are not implemented yet. A permitted Tab does not mean its business functionality already has an API endpoint.
Use the tenant API domain: https://api.{name}.kombine.technology in production or https://beta.api.{name}.kombine.technology in beta. Names: team, electrolux, portal, washco, finelec and nortec. Each hostname is bound to server-configured tenancy. KIDs, form fields and forwarded host headers cannot change the tenant. Tokens apply only to their tenant and environment; log in separately when switching. Unknown hosts return HTTP 400. Beta currently uses the same tenant databases as production.
Select Public v1 (no login) in Swagger for anonymous status, statistics and the purchase map. Download its contract as Public OpenAPI JSON. Portal integrations v1 contains manager login and manager operations. This split does not change routes, operation IDs or access control. Clients generating public calls from the previously combined v1 document must now also import the Public document.
Purchases in the last hour
GET /api/v1/public/statistics/purchases requires no login. Its Swagger operation is GetPublicPurchases; the JavaScript client provides client.getPurchases().
Counts rows in the site's A{tenant}.Log1Hour using UserId >= eUserId.Users AND UserId <= eUserId.UsersLast AND Text NOT LIKE '%E' AND Amount < 0. These are purchases, not distinct customers. E matching follows the table's collation. The shared enum defines the inclusive user range, currently 1001–99999. No bank filter is added. The response also includes amount = -SUM(Amount)/100 and currency = MAX(Currency). No purchases returns zero and null. MAX(Currency) assumes a common currency among purchases; no conversion is performed.
The database maintains Log1Hour with cleanup every minute. This operation uses its contents exactly as the agreed SQL does, so the actual window depends on that cleanup. sinceUtc is the nominal start one hour before measuredAtUtc; lookbackHours is 1. tenantKid identifies the site and count is the total. Clients cannot override the tenant or filters.
Results are cached for one minute per API instance. The card refreshes automatically every minute while the page is open. Concurrent requests share one database query. HTTP 503 with Retry-After: 60 means unavailable, not zero. The usual CORS policy applies to JavaScript on other sites.
const response = await fetch('https://api.team.kombine.technology/api/v1/public/statistics/purchases');
if (!response.ok) throw new Error('HTTP ' + response.status);
const sample = await response.json();
console.log(purchases.count, purchases.amount, purchases.currency);
Public data without login
GET /api/v1/public/statistics/active-users — Active-user count for the site's tenant. Swagger: Public → GetPublicActiveUsers.
The count includes distinct bank/user pairs in the tenant's current Log7, with user IDs 1001 through 99999 inclusive, bank ID at least 1000, and MS2000 strictly newer than 100 days before the UTC measurement time. Multiple rows for one pair count once; the same user ID in two banks counts twice. Activity means a recent Log7 record, not necessarily a login. User Enabled and Deleted settings are not additional filters.
This aggregate is public and independent of manager permissions. No individual accounts or per-bank counts are exposed. The site chooses the tenant; this operation accepts no KID, date window, or other filters.
tenantKid identifies the site's tenant, count is the total, lookbackDays is 100, sinceUtc is the exclusive cutoff, and measuredAtUtc is the measurement time. Results are cached for up to 5 minutes per API instance. Concurrent visitors share one query, with no background polling. Database failures return HTTP 503 and Retry-After: 60; display unavailable, not zero. A successful response with count 0 means a genuine zero.
const response = await fetch('https://api.team.kombine.technology/api/v1/public/statistics/active-users');
if (!response.ok) throw new Error('HTTP ' + response.status);
const statistics = await response.json();
console.log(statistics.count, statistics.measuredAtUtc);
The JavaScript client also provides client.getActiveUsers(). For JavaScript on another site, configure the client's exact origin in the API's Cors:AllowedOrigins as described below.
Managers may share an email address if their passwords differ. Login selects the matching email/password pair; multiple accounts with the same pair are rejected with 401.
Failed login attempts may be recorded as internal JSON diagnostics. Client status codes and responses are unchanged; internal reasons such as duplicate email/password pairs are not disclosed by login. Operators can correlate the request trace ID with API logs. Never include passwords or tokens in error reports.
Login and manager settings use the site's own A{TenantId:D4}.Log7, BankId 0. D4 pads the tenant ID to at least four digits: Team 166 uses A0166.Log7. The server's PortalSite:TenantId selects the tenant; there is no fallback to another tenant's manager database.
A manager must have Enabled = 1. An absent Deleted row (or SQL NULL) defaults to 0: not deleted. Present invalid Deleted values and positive deletion timestamps still block login.
1. From API address to your first login
The API base address for this tenant site is https://api.team.kombine.technology. Use the API address, not the portal address.
Examples automatically use the address of the API site serving this guide. Beta shows the beta address. Without JavaScript, the Team production address is shown; check the address before use. You cannot switch tenants by adding a field, query parameter, or header. The server determines the site and permissions.
| Request | Purpose | Authentication |
|---|---|---|
GET /api/v1/public/statistics/purchases | Purchase count for the last hour | None |
GET /api/v1/public/statistics/active-users | Active-user count for the site's tenant | None |
GET /api/v1/status | Check whether the API responds. Does not check MySQL. | None |
POST /api/v1/session/login | Exchange a manager email and password for a temporary access token. | Email and password in JSON |
GET /api/v1/session/me | Retrieve your own profile and current permissions together. | Access token |
Try it without writing a program
- Open Swagger and select Portal integrations v1. Swagger is a web page for reading about and trying API requests.
- Expand GetPortalStatus, select Try it out, then Execute. HTTP 200 means the request succeeded.
- Expand LoginManager. Replace the example email and password with your manager’s credentials and select Execute. All example credentials are fictitious and cannot log in.
- Copy only the
accessTokenvalue from the response, without quotation marks. Click Authorize, paste it under ManagerBearer, and authorize. Swagger adds theBearerprefix. - Run GetCurrentManager. Its response provides the information for your portal’s profile and access cards.
A manager must be enabled and not deleted. Missing Tabs or bank access do not prevent login itself; explain the missing access in your portal.
The HTTP requests
POST https://api.team.kombine.technology/api/v1/session/login
Content-Type: application/json
Accept: application/json
{"email":"[email protected]","password":"<your password>"}
Send the original password over HTTPS. The API verifies it; do not encode or hash it in the client.
{
"accessToken": "<your access token>",
"expiresIn": 3600,
"tokenType": "Bearer"
}
GET https://api.team.kombine.technology/api/v1/session/me
Authorization: Bearer <your access token>
Accept: application/json
The token lasts one hour from login. Treat it as a secret string: it is not a JWT to decode. Reuse it for requests and log in again when it expires. There are no refresh tokens or single-token revocation endpoints yet. On logout, your client discards its token; an existing copy may remain usable until expiry or account/password revocation.
A complete PowerShell 7 example
Copy this block into PowerShell 7. The dialog asks for the manager email and password. It displays the profile without printing the password or token. For local development, trust the .NET development certificate using dotnet dev-certs https --trust.
$api = 'https://api.team.kombine.technology'
$credential = Get-Credential -Message 'Manager email and password'
$session = $null
$body = $null
$headers = $null
try {
$body = @{
email = $credential.UserName
password = $credential.GetNetworkCredential().Password
} | ConvertTo-Json -Compress
$session = Invoke-RestMethod "$api/api/v1/session/login" `
-Method Post -ContentType 'application/json' -Body $body -TimeoutSec 15
$headers = @{ Authorization = "Bearer $($session.accessToken)" }
$profile = Invoke-RestMethod "$api/api/v1/session/me" `
-Headers $headers -TimeoutSec 15
$profile | ConvertTo-Json -Depth 6
}
catch {
if ($_.Exception.Response) {
Write-Warning "HTTP $([int]$_.Exception.Response.StatusCode). See the error table."
} else {
Write-Warning 'Could not reach the API. Check the address, connection, and certificate.'
}
}
finally {
$body = $null
$headers = $null
$session = $null
$credential = $null
}
2. Understand the profile and permissions
This response is fictitious. Always use the actual values returned by the API.
{
"kid": "3E7Q46o3B9ACA01h",
"retentionDays": 30,
"organisation": "Example organisation",
"name": "Example manager",
"icon": "house",
"tabs": [4, 12],
"hasBankAccess": true,
"tabDetails": [{"id": 4, "name": "Bank1"}, {"id": 12, "name": "Dashboard1"}],
"resourceGrants": [{"kid": "3E7Q14o2Ab", "scope": "Bank"}],
"navigationBanks": [{"kid": "3E7Q14o2Ab", "name": "Example bank", "icon": "house"}],
"operationPermissions": [
{"resource": "Bank", "level": "Read", "canRead": true, "canWrite": false, "canCreate": false},
{"resource": "Location", "level": "Write", "canRead": true, "canWrite": true, "canCreate": false},
{"resource": "Unit", "level": "Create", "canRead": true, "canWrite": true, "canCreate": true},
{"resource": "User", "level": null, "canRead": false, "canWrite": false, "canCreate": false}
]
}
| Field | Meaning and use |
|---|---|
organisation | Optional organisation from the manager's eSetting.Organisation in A{TenantId:D4}.Log7, BankId 0. Empty string when absent. The portal displays it below the name when nonblank. Display only; grants no access. Read in the same query and included in the manager's cache of at most 60 seconds. |
retentionDays | Days after deletion during which the manager may see a deleted bank, location, unit, user or reservation. Read from eSetting.RetentionDays in A{TenantId:D4}.Log7, BankId 0. Missing, negative or invalid values default to 0, hiding deleted objects. This does not expand Kids, Tab or operation permissions and does not schedule physical deletion. Cached with the manager for at most 60 seconds. GetBankUsers enforces this limit. Future details, counts and searches must enforce it too. |
kid | The manager ID as a string. Store and pass it unchanged. me returns only the signed-in manager. An ID does not grant access. |
name, icon | Display name and icon identifier. The name can be empty. house can be displayed from https://static.kombine.services/icon1/house.svg. Use a known valid icon name or a default icon; never insert raw markup or an arbitrary URL from the field. |
tabs, tabDetails | Permitted pages, called Tabs. IDs correspond to eTab; names are stable enum identifiers, not translated page titles. Use IDs as keys and translate your own display text. Do not interpret unknown IDs as known permissions. |
hasBankAccess | Whether at least one bank or location grant applies on this site. This is not blanket access to data. |
resourceGrants | The specific areas the manager may access, explained below. Does not contain bank names or actual bank records. |
operationPermissions | Four independent operation permissions: Bank, Location, Unit, User. Use the computed canRead, canWrite, canCreate flags to display relevant actions. |
Which banks and locations?
Each entry in resourceGrants applies only to its tenant. Multiple entries grant access to multiple areas.
scope = "Tenant": the KID identifies the site's entire tenant. Display “All banks” and “All locations”.scope = "Bank": the KID identifies one bank and all its locations.scope = "Location": the KID identifies one particular location in a bank.
scope describes the extent of access. Use the ID unchanged; do not derive permissions or object names from the string.
Working with KID strings
A KID is an ID returned in a kid field. Treat it as an ordinary string in every programming language. Store and send the complete value unchanged, preserving letter case. Do not decode, split or construct it yourself. No Kombine library is required.
Use the bank kid from navigationBanks for bank requests, the location kid from the location list for location requests, and the user kid from the user list for user requests. Use encodeURIComponent(kid) in JavaScript when inserting an ID into a URL. Use name for display and scope for the extent of access.
An ID is neither a login token nor a permission. The server checks the site and manager permissions on every protected request. An ID cannot switch tenants or grant more access.
This contract revision replaces the separate userId, tenantId, bankId, and locationId fields with KID strings. Update clients that used the old fields. Existing login and profile routes are unchanged.
What may the manager do?
level | Read | Modify | Create |
|---|---|---|---|
Read | Yes | No | No |
Write | Yes | Yes | No |
Create | Yes | Yes | Yes |
null | No | No | No |
A missing or empty permission setting becomes Read on the server. An invalid nonempty setting yields null and no operations. Operation permissions never expand access to Tabs, banks, or locations. Write/Create permissions do not imply a delete permission.
Show “You do not have access to any banks yet” when hasBankAccess is false. Show “You do not have access to any Tabs yet” when tabs is empty. Both messages may apply. A network error or HTTP 503 must be shown as a retrieval error rather than missing permissions.
Client controls help the user navigate. The API must enforce account state, site, Tab, the actual object’s scope, and the operation before returning or changing business records. Editing client JSON or a link must never grant more access.
3. Errors and client actions
Check the HTTP status first. Login can return HTTP 403 with a stable code, for example {"code":"disabled"}. Other errors normally use Problem Details with status, title, and possibly traceId; validation errors can also contain errors. A proxy or web server can return an empty body or HTML, so error handling must not require JSON.
| Status / code | Meaning | Client action |
|---|---|---|
| 400 | Invalid JSON, email, or missing fields. | Correct the input. Email is limited to 254 characters; password to 1,024. |
| 401 on login | Credentials do not match a valid manager. | Show “Incorrect email or password.” Do not retry automatically. |
401 on me | Missing, invalid, or expired token, or a changed account/password. | Discard the token and return to login. This response does not reveal the exact account state. |
403 / disabled | The manager is disabled. | Explain that the manager is disabled and suggest contacting the administrator. |
403 / deleted | The manager is deleted. | Explain that the manager is deleted and suggest contacting the administrator. |
403 / account-settings | Enabled is missing or invalid, or a present Deleted value is invalid. | Explain that account setup is incomplete and needs administrator attention. |
| 413 / 415 | Oversized login body / unsupported content type. | Send only email and password as application/json. Login body limit: 8,192 bytes. |
| 429 | Too many login attempts. | Wait at least Retry-After seconds (currently 60). Default to 60 seconds if absent. |
| 503 | Required data cannot be loaded. | Display a temporary service error. Offer a retry after a pause. |
Login reveals a 403 reason only after verifying the password. Translate the codes in your own interface. Do not depend on English error titles or a particular traceId.
4. Your own portal
Complete JavaScript example
Open the working JavaScript example. It includes API address, email, and password fields and buttons for status, login, profile refresh, and logout. It uses ordinary fetch without npm packages or a framework.
To use it in your portal, copy portal-api.mjs into your JavaScript folder. It is a small optional example; direct fetch calls also work, as shown below. Create one client per user and API site:
import { createPortalClient } from './portal-api.mjs';
const portal = createPortalClient('https://api.team.kombine.technology');
// Obtain email and password from your login form.
await portal.login(email, password);
const manager = await portal.getProfile();
// Display manager.name, manager.tabDetails, and manager.operationPermissions.
// Call portal.getProfile() for manual refresh and portal.logout() for logout.
Load your own script with <script type="module" src="./app.mjs"></script>. Host the example files on your own web server; do not open them using file:// or import the module directly from another API origin. The same module works in Node.js with built-in fetch. Server-side JavaScript does not need CORS.
The client keeps only the token in memory, reuses it, clears it after HTTP 401, and provides errors with status, code, and retryAfter for HTTP 429. Network, certificate, timeout, and CORS failures may have no HTTP status. There are no automatic retries or polling. Do not share a client instance between users on a Node.js server.
A portal with its own server
Have your server call the API and keep the access token in each user’s protected session. This is also how the official Blazor portal works. Never share one manager session between users. The browser uses your portal’s own cookie/login while your server sends the bearer token to the API. This does not require CORS.
A browser calling the API directly
This is also supported. When your portal uses a different origin from the API, the API administrator must add its exact origin to Cors:AllowedOrigins. An origin is the scheme, hostname, and optional port, without a path or trailing slash. The default list is empty.
{
"Cors": {
"AllowedOrigins": ["https://my-portal.example", "https://localhost:5173"]
}
}
This is server configuration and requires a restart. The environment variable for the first origin is Cors__AllowedOrigins__0. Only listed origins can read integration responses through a browser. Authentication and permissions still apply. CORS is a browser rule, not access control for server programs.
If your development site runs at http://localhost:5173, add exactly that address; https://localhost:5173 is a different origin. Use the API’s HTTPS address. The browser automatically sends OPTIONS before applicable requests; the API handles it. Do not use mode: 'no-cors': your code would be unable to read the JSON response.
Call this small browser function with the values from your login form. It returns the profile without persisting the token in localStorage or placing it in the URL.
async function loginAndLoadProfile(apiBaseUrl, email, password) {
const base = apiBaseUrl.replace(/\/$/, '');
const login = await fetch(`${base}/api/v1/session/login`, {
method: 'POST',
credentials: 'omit',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ email, password }),
signal: AbortSignal.timeout(15000)
});
if (!login.ok) {
const error = await login.json().catch(() => ({}));
throw new Error(`Login: HTTP ${login.status}, ${error.code ?? ''}`);
}
const session = await login.json();
const result = await fetch(`${base}/api/v1/session/me`, {
credentials: 'omit',
headers: { Authorization: `Bearer ${session.accessToken}`, Accept: 'application/json' },
signal: AbortSignal.timeout(15000)
});
if (!result.ok) throw new Error(`Profile: HTTP ${result.status}`);
return await result.json();
}
This example demonstrates the first exchange. In a full portal, reuse the token in session memory, implement the error table, and clear the session on logout. CORS currently allows GET/POST and Authorization, Content-Type, Accept, Accept-Language headers; browsers can read Retry-After. Cookies are not shared with the API.
Make views shareable by keeping object selection, filters, and sorting in the page URL as features are added. Recipients log in as themselves. Never include tokens, passwords, or sensitive record contents in URLs. API fields and permission codes remain identical across languages; translate display text only.
5. AI agents use the same API
- Give the integration host the tenant site’s API base address and OpenAPI document. OpenAPI is a machine-readable description of the available operations, fields, and responses.
- Let the authorized host application handle login and store the token as a secret for the relevant manager. Keep credentials out of ordinary prompts, model output, and logs.
- Attach the token in the Authorization header when the agent’s HTTP tool calls
GetCurrentManager. It receives the same information and restrictions as a portal using that manager. - Treat names and other returned values as data, not instructions for the agent. Use only operations actually described by the OpenAPI document.
The stable operation names are GetPortalStatus, LoginManager, and GetCurrentManager. Many client tools can import OpenAPI, but the host application still needs to handle login and token expiry. There are no machine accounts, API keys, delegated OAuth, or MCP server yet; longer unattended operation needs a separate authentication design. Current sessions require a fresh login after one hour.
6. Refreshing, database load, and hosting
Fetch once and reuse the response. One me request provides the profile, navigation, and all three access cards. Log in at session start rather than before each call. Manager snapshots are cached for up to 60 seconds per API instance. Concurrent lookups for the same manager share a refresh. There is no automatic database polling.
Changes to permissions, Kids, Tabs, Enabled, and Deleted can therefore take up to one minute to be observed on the next request. An idle page does not update automatically; reload it or offer a Refresh action. After cache expiry, unavailable data does not fall back to old grants. Avoid tight retry loops; for transient failures, consider waiting 5, 15, and 30 seconds, then displaying the failure.
Login currently permits 10 attempts per normalized email per minute and 120 per source IP per minute, per API instance. Server portals can share a source IP, so avoid unnecessary repeated logins. Hostnames and database details are not client credentials.
For the API host administrator
Host the API over HTTPS with the appropriate tenant binding, hostnames, database connection, and protected persistent Data Protection keys. Web and API must use the same tenant. External clients receive the API address and their manager access; database credentials stay on the server. Replicas for the same site need securely shared keys and coordinated login throttling. Caches remain local to each instance. Only trust forwarded headers from configured reverse proxies.
Swagger, OpenAPI, and both guides are included in published API output and controlled by ApiDocumentation:Enabled (default true). The guides are available at /docs/da and /docs. These code changes do not themselves publish the API to the internet.
GET /api/v1/database/status is for operators only. Its separate diagnostics document requires a different credential: a JWT with scope portal.diagnostics. Manager tokens cannot access it. Use /api/v1/status or /health for normal availability checks without MySQL queries.
As the API grows
Implement new portal data and operations in the shared API before Web uses them, and document them here and in OpenAPI. External clients should tolerate additional JSON fields and future enum values without granting extra access. Do not silently change the meaning of existing v1 fields; incompatible contract changes require a new version. Large lists need server-side filtering and pagination when introduced.
Bank names in navigation
The existing GET /api/v1/session/me (GetCurrentManager) includes navigationBanks. Reuse your bearer token and read profile.navigationBanks in JavaScript. Each item has kid, name, and icon; no numeric identifiers or extra request are needed. Use the KID as the item identity and copy that exact string when sharing an identifier.
Labels require an active account, at least one permitted Tab, Bank Read, and a bank or location grant in this site. A location grant allows its parent bank's navigation label only, never all bank data. The array is empty for all-bank access (enumeration is deferred), no Tabs, no scopes, or invalid Bank permission. Each child Tab still requires its own authorization for actual operations.
Names and icons come from the site's Log24, EntryType Settings (2), LocationId 0, UnitId 0, using eSetting.Name (99) and Icon (37). Missing values are empty strings: show an unnamed-bank label and a local icon fallback. Render text as text, never HTML. For a safe icon identifier such as house, use https://static.kombine.services/icon1/house.svg. The portal shows the bank KID on hover and offers a copy action on right-click.
Labels are cached for one minute per bank per API instance, shared between managers; cache misses are batched in groups of at most 64 banks. Authorization is rechecked independently with the bounded manager snapshot. No bank enumeration occurs for tenant-wide grants. A storage failure returns 503: do not interpret it as an empty permission list. Retry later; invalid/expired sessions return 401. Navigation labels do not implement bank detail or deleted-record listings.
tabDetails[].icon comes from AttributeMetaIcon on the corresponding eTab, for example bank_building. Use the same safe icon URL pattern as banks. An empty string means no icon; display a local fallback. Icons come from the shared enum without database queries.
userKid selects exactly one resident in the bank: await client.getBankUsers(bankKid, { userKid: residentKid }). Use a canonical User KID from a previous response. It must belong to the site tenant and specified bank and cannot be combined with filter or cursor (400). The same manager, Tab, User Read, location and RetentionDays checks apply. The response has zero or one item and no continuation cursors. Missing or invisible users return an empty list without revealing whether they exist. The portal shareable view uses /banks/{pageKid}, with both Tab=Users2 and the resident UserId in its bank page KID. Legacy ?resident={userKid} links redirect; browser workspace bookmarks grant no permissions.
Wildcards in filter: * matches zero or more characters and ? matches one character. Examples: filter=1568-*002* or filter=Anna?*. Matching remains substring-based in Number OR Name, including plain text without wildcards. Other characters, including SQL characters % and _, are literal. URL-encode the filter with URLSearchParams or encodeURIComponent. Matching runs in API memory against the existing cached index; no wildcard SQL is generated.
filter matches a substring in number OR name using culture-independent case-insensitive comparison. Maximum 200 characters; surrounding whitespace is trimmed. An empty filter returns the full authorized list. Filtering precedes paging and reuses the shared cached sorting index, including identity mode. Keep filter with the cursor; changing it requires a request without a cursor, otherwise 400 is returned. Permissions and RetentionDays still apply. Example: await client.getBankUsers(bankKid, { sort: 'name', direction: 'asc', filter: 'anna', pageSize: 25 }). URL-encode filter text. Shared links contain the search text.
icon contains the user setting eSetting.Icon (37), normalized to a valid eIcon name. Missing, empty, unknown values and none default to user. Stored enum names and numeric enum values are supported. Display it from https://static.kombine.services/icon1/{icon}.svg. The icon is fetched with the page settings under the same authorization and cache; it adds no separate database query per user.
Set sort=number|name|location|deleted and direction=asc|desc. Ordering applies to the entire authorized list, including progressively loaded pages. Omitting sort preserves legacy identity order (asc only). Numeric numbers sort numerically before text numbers; names and text numbers use language-independent ordinal, case-insensitive comparison. Location means the lowest visible Access/NoAccess location number. Empty values and non-deleted users come first in ASC and last in DESC. Deleted users sort by deletion time. User identity breaks ties.
const page = await client.getBankUsers(bankKid, {
pageSize: 25, sort: 'name', direction: 'asc'
});
const next = page.nextCursor
? await client.getBankUsers(bankKid, {
pageSize: 25, sort: 'name', direction: 'asc', cursor: page.nextCursor
})
: null;
Keep sort, direction and pageSize during traversal. When changing order, restart without a cursor. A cursor for another sort or direction returns 400. Sorted cursors are positions in the current authorized list; data or permission changes can shift positions between requests. Shared links never grant the sender's permissions.
Sorting uses a shared four-setting index cached for up to 60 seconds across managers and sort choices. The API filters permissions and sorts in memory; only the selected page fetches the remaining details. There is no COUNT, SQL OFFSET or query per user. A cold cache scans the bank's ordinary users; very large banks may hit the timeout and return 503. Avoid immediate automatic retries. Identity mode retains its bounded 1,000-candidate scan and scanLimitReached behavior.
The official portal uses GetBankUsers for progressive loading while scrolling. External portals can do the same: request one nextCursor at a time, reuse the result and stop at null. Stop automatic loading on errors and offer an explicit retry. Every request is authorized, including cursors received in a shared link from a colleague.
Users in a bank (Users2)
GET /api/v1/banks/{bankKid}/users?pageSize=25 · operation GetBankUsers. Take the bank KID from navigationBanks or a Bank scope in resourceGrants. Send the manager bearer token. No separate tenant, bank or user IDs are accepted.
const page = await client.getBankUsers(bankKid, { pageSize: 25 });
for (const user of page.items) console.log(user.kid, user.name, user.number);
if (page.nextCursor) {
const next = await client.getBankUsers(bankKid, {
pageSize: 25, cursor: page.nextCursor
});
}
The response contains items, previousCursor, nextCursor and scanLimitReached. Each user has kid, name (99), number (1824), deletedAt (1996, UTC or null), locations (1809, KID and Access/NoAccess), tags (2977, KID and eTagState) and attributes (2978, eUserAttribute name and value; negative means no numeric value). Missing text/lists are empty; missing Deleted means not deleted.
Requires an active manager, Users2 (53), User Read and a matching resource scope. Tenant/bank grants include ordinary bank users. Location grants include only users associated with at least one permitted location, in either Access or NoAccess state; other locations are removed from the response. Name, number, tags and attributes are shared bank-level user data. RetentionDays limits deleted-user visibility; malformed deletion settings hide the user. The inclusive eUserId.Users–UsersLast range excludes manager and service accounts.
pageSize is 1–200 (default 25). The default identity mode orders by ascending user identity. Copy each returned cursor unchanged; null means no continuation in that direction. Cursors can be shared, but never grant access. Recipients use their own permissions and may see different contents. Concurrent changes mean pages are not a frozen snapshot.
In identity mode, to limit MySQL load, batches are cached for at most 60 seconds, without a full count or OFFSET. At most 1,000 candidates are examined per request. With scanLimitReached=true, a page can be short or empty: follow its continuation cursor. Do not automatically poll every page.
400: invalid KID, cursor or page size (restart at the first page). 401: sign in again. 403: missing Tab, scope or User Read. 503: temporary storage failure; show an error and retry later, not an empty list. Tokens are never included in shareable portal URLs. Browser JavaScript requires an allowed CORS origin as described above.
Users2: Each locations item also includes icon, the location eIcon name from eSetting.Icon in Log24. Missing or invalid icons default to house. Icons are cached for up to one minute. state remains Access or NoAccess. The portal displays the icon with state as a data attribute and location number/state/KID in its tooltip.
Bank overview locations
GET /api/v1/banks/{bankKid}/locations (operationId: GetBankLocations) returns an array of {kid,name,icon}. Use a plain bank KID and your manager bearer token.
const response = await fetch(`${api}/api/v1/banks/${encodeURIComponent(bankKid)}/locations`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Location request failed: ${response.status}`);
const locations = await response.json();Requires an active manager, at least one assigned Tab, Location Read (also included in Write/Create), and matching site/bank/location access. Location-only grants return only those locations. Deleted locations respect RetentionDays. 400: invalid/wrong-site KID; 401: sign in again; 403: insufficient access; 503: retry later. Data is cached for up to 60 seconds; authorization is checked on every request. Results are ordered by location number, without pagination. Only locations with a Name, Icon or Deleted setting in Log24 are discoverable. Missing or invalid icons default to house. Use kid as the location ID and name as its display name.
Location overview and units
GET /api/v1/locations/{locationKid}/units, operationId GetLocationUnits. Returns {location:{kid,name,icon},items:[{kid,name,icon}]}. Use a canonical location KID from the bank location list or a user's locations; user location entries now also include the location name.
const response = await fetch(`${api}/api/v1/locations/${encodeURIComponent(locationKid)}/units`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Unit request failed: ${response.status}`);
const { location, items } = await response.json();Requires an active manager, at least one Tab, Location Read and Unit Read (Write/Create include Read), plus access to this site and bank or exact location. Every request rechecks access. Both location and unit deletion follow RetentionDays; missing Deleted means not deleted. 400: invalid KID/site; 401: sign in again; 403: insufficient rights; 404: location missing or no longer visible; 503: retry later. At most 255 units, ordered by unit number without pagination. Empty items is a valid result.
Log24 data is cached for up to one minute. Objects need Name, Icon or Deleted to be discoverable. Missing names are empty; missing/invalid icons use house. Web uses the shareable route /locations/{locationKid}. Workspace shortcuts are stored per manager and browser tab and never grant access; the cross only removes the shortcut.
Unit overview uses the shareable route /units/{unitKid}. The portal reuses GetLocationUnits and selects the exact unit KID from the API-authorized results. Missing or invisible units reveal no details. Additional unit functionality is not yet implemented. Unit workspace shortcuts are nested under their location; removing an open shortcut returns to the location and never deletes the unit.
Unit name language
Send Accept-Language: en-GB on each request. Language is not bound to login or token. Known eLocalization placeholders such as [455] in unit names are resolved from shared resources; surrounding text and unknown IDs are preserved. Content-Language reports the selected language. The ten portal languages and regional variants are supported; no/nn map to Norwegian Bokmål and pt-BR to pt-PT. Missing, malformed or unsupported language defaults to en-GB. Quality preferences are honored and q=0 excluded. Raw data is cached before translation, so languages never mix across users and require no extra SQL.
const response = await fetch(`${api}/api/v1/locations/${locationKid}/units`, { headers: { Authorization: `Bearer ${token}`, "Accept-Language": "en-GB" } });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const overview = await response.json();Current eCycle
GetLocationUnits now returns cycle (stable eCycle name) and cycleText (Accept-Language) per unit. Both are null for missing/invalid values. The newest MS2000 from eSetting.Cycle (1619, Settings) and eState.Cycle (20, States) wins; States wins ties. An invalid newest value never falls back to an older value. Unit data is now cached for at most 10 seconds; location labels and manager permissions for up to one minute. Both SQL branches are bank/location scoped. The portal refreshes every 10 seconds, pauses in hidden tabs, prevents overlapping requests and hides stale details on failure. External clients can repeat this GET with the same bearer token and Accept-Language every 10 seconds.
Settlement: viewing, history and downloads
Every call requires Authorization: Bearer TOKEN, the Settlement2 Tab, Bank Read, User Read and access to the entire bank. Location-only access cannot expose bank-wide settlement data. Permissions are checked on every request with a manager snapshot cached for at most one minute. These GET operations never close, undo or modify a settlement.
| Operation | GET path |
|---|---|
| GetBankSettlements | /api/v1/banks/{bankKid}/settlements?beforePeriod=123 |
| GetBankSettlementPeriod | /api/v1/banks/{bankKid}/settlements/{period} |
| DownloadBankSettlement | /api/v1/banks/{bankKid}/settlements/{period}/download?format=XLS |
Omit beforePeriod initially. History returns up to 25 closed periods, nextSettlement and nextBeforePeriod. Pass the next cursor unchanged; null means the end. Unknown dates and values are null. Dates use UTC. Period 0 is the provisional current period, available through details and downloads; it can change until settlement.
Details contain sourceEntries, includedEntries, groups and formats. Each group has group, currency, entries and amountMinor. Amounts are signed database values in minor units, not formatted currency amounts. Different currencies are never added together. Stored history totals may differ from export totals.
ChargePoint_58/TimeNew power consumption is excluded; cash banks also exclude Month and Transfer. Group priority is ETest, EInstaller, EGuest, configured number masks (U), EDate, then LR. Masks use % for multiple characters and _ for one character. Valid legacy entries classified as Unknown retain their recorded amounts. Current user numbers, names, tags and attributes are used, so regenerating a historical file may differ from its original version. Settlement is exempt from RetentionDays: deleted users remain included in period details and downloads regardless of deletion age. Normal manager, Tab, bank and read permissions are still enforced.
The ZIP contains one file per group and currency plus a reconciliation manifest.json. Empty periods contain only the manifest. Text formats may omit groups with no exportable amounts; group totals remain in the manifest. XLS produces real .xlsx workbooks with Number, Amount and UserId following the export convention; UserId in this compatibility file is numeric, while HTTP object identifiers are KIDs. Other formats use UTF-8 text with CRLF. Excel keeps the database amount sign; NAVISION, for example, reverses it.
Formats: XLS, ATB, BL, DEAS, FRUEHØJGAARD, HEIMSTADEN, LEJERBO, MD90_1, MD90_3, MD90_3_minus, MD90_3_plus, MD90_3_AABKBH and NAVISION. MD90_3 is defined for banks 1001 and 1068 only. NIRAS and ROBERT are obsolete. HUMAN, KMD, LYKKEBO and MD90 are not offered because the reviewed shared code contains no implemented export for them. Format identifiers are case-sensitive; use the list returned in the details.
const headers = { Authorization: `Bearer ${token}` };
const base = `/api/v1/banks/${encodeURIComponent(bankKid)}/settlements`;
const details = await fetch(`${base}/12`, { headers });
if (!details.ok) throw new Error(`HTTP ${details.status}`);
const period = await details.json();
const download = await fetch(`${base}/12/download?format=XLS`, { headers });
if (!download.ok) throw new Error(`HTTP ${download.status}`);
const url = URL.createObjectURL(await download.blob());
const link = document.createElement('a');
link.href = url; link.download = 'settlement-12.zip'; link.click();
setTimeout(() => URL.revokeObjectURL(url), 60000);
This example assumes the same origin. External browser portals use the full API base address and an allowed CORS origin. 400 means invalid KID/period/format, 401 requires login, 403 means denied permissions, 404 means an unknown closed period, 422 means data cannot be exported safely, and 503 means temporarily unavailable data. Causes of 422 include invalid transaction codes, invalid numbers/currency, duplicate export numbers across users, field width overflow or more than 100,000 entries/10,000 users. No partial files are returned. Correct data/format instead of retrying 422. Back off before retrying 503.
History and period sources are cached for at most one minute, coalescing concurrent reads. Details load only when a period is selected. No background database reads or MySQL writes occur. Legacy readiness calculations and automatic jobs have not been migrated; this API does not claim a bank is ready to close.
Disp73: Public purchase map
GET /api/v1/public/displays/disp73 · operation GetPublicDisp73 in the Public Swagger definition. No login is required. The server selects the tenant; query parameters cannot change tenant, bank or time boundary. ?limit=100 is optional (default 100), accepts 1–200 and returns HTTP 400 for invalid values. The value is bound to the SQL @limit parameter; caching is separate per limit.
const response = await fetch('https://api.team.kombine.technology/api/v1/public/displays/disp73');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const sample = await response.json();
for (const p of sample.items) {
// Ignore known p.kid. Spread new coins evenly across the next 10 seconds.
queueCoin(p.kid, p.latitude, p.longitude, p.timestampUtc, p.amount);
}
The response is {measuredAtUtc, refreshAfterSeconds: 10, items: [...]}. refreshAfterSeconds is always 10. The items array contains {kid, latitude, longitude, timestampUtc, amount}, newest first. It selects at most the latest 200 purchases from the maintained Log1Hour table without a timestamp filter. Missing/invalid coordinates omit a row, so fewer may be returned. Transfers and rows whose Text ends in E are excluded.
timestampUtc is ISO 8601 UTC: 2000-01-01T00:00:00Z + original MS2000 (without an offset). kid is the canonical transaction KID containing original time and tenant/bank/location/unit; it grants no permissions. User IDs, names, tags and transaction text are omitted. Amount is positive major units (minus Amount / 100), without currency conversion or a currency field.
Fetch every 10 seconds without overlapping requests. Ignore existing KIDs, merge with retained events and remove the oldest above 100. New coins are spread evenly across the next ten seconds, each falling once; timestampUtc is only used for ordering and removing the oldest events. Hidden tabs suspend requests/animation and resume on return. Reduced-motion settings disable falling animation. The API shares a ten-second cache. An empty array means no displayable events. HTTP 503 means unavailable data: keep existing coins and wait 30 seconds (Retry-After). External browser clients need an allowed CORS origin. Public display KIDs do not authorize protected bank operations.