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.
Addresses and automatic coordinates
Available in this source branch; not yet deployed. These operations support Bank, Location, Unit and resident User. Tenant, manager and installer addresses are excluded. Supply one canonical object KID from the current site. Address values are the object's own settings; parents are not inherited.
Operation ID
Method and path
Purpose
GetObjectAddress
GET /api/v1/addresses/{kid}
Read settings and revision
UpdateObjectAddress
PUT /api/v1/addresses/{kid}
Save Address and Zip, then resolve coordinates
LookupObjectCoordinates
POST /api/v1/addresses/{kid}/lookup
Deliberately resolve the current address again
SetObjectCoordinates
PUT /api/v1/addresses/{kid}/coordinates
Save a manual coordinate pair
SetObjectCoordinateProvenance
PUT /api/v1/addresses/{kid}/provenance
Change coordinate provenance
Requires an active manager, an assigned Tab (Users2 for residents), a matching resource grant, Bank Read and the object's Read permission. Unit also requires Location Read. Every write additionally requires the object's Write permission. Bank and User need a bank-wide or tenant-wide grant; location-only grants cannot edit shared bank or resident addresses. Objects and parents must exist and not be deleted; disabled locations require tenant-wide access. Rights are rechecked under storage locks before each transaction.
Use your site's HTTPS base URL as API, the returned canonical object ID as KID, and refresh the revision between commands. Both address strings are required, may be empty and are trimmed; maximum lengths are 512 and 64 characters. Control characters are rejected. A change to either Address or Zip triggers at most one coordinate lookup. A bare postal code additionally triggers one postal-city lookup, including in manual mode. The trusted site tenant selects the country: Team, Nortec and Electrolux use DK (four digits), Washco uses GB (full alphanumeric postcode, for example SW1A 1AA), and Finelec uses FI (five digits, for example 00100). Other tenants skip postal completion. The country cannot be selected by the client. Address coordinate lookups also use the mapped country as their region bias. An exact, unambiguous postal-city result is saved in Zip as, for example, 7470 Karup J. Existing city text is preserved; missing, ambiguous or failed postal results leave the input unchanged. Manual coordinates are never changed by postal completion. If the postal response lacks a city, the full address response may supply it when country and postal code match exactly. In manual mode this address lookup completes Zip only; it preserves coordinates and provenance. Neither request is retried. Use the returned zip and revision for subsequent edits. Unchanged inputs skip lookup; use the explicit lookup operation to retry. No background retries are scheduled.
The address transaction inserts both fields and clears old automatic Latitude and Longitude and sets AutoLatitudeLongitude to 0. Successful lookup inserts the new pair and UTC month (1–12) together. Provider failure leaves automatic coordinates empty. Manual provenance 30 protects the pair during address changes. Explicit lookup replaces a manual pair only on success. Concurrent edits return Superseded without overwriting newer values. History is append-only: bank Log2 for Bank/Location/Unit, bank Log7 for User, with Sync=0; success confirms storage, not device acknowledgement.
LookupObjectCoordinates also supports a Location with an empty Address: it looks up the stored Name plus Zip. Only on success is Name saved as Address in the same transaction as Latitude, Longitude and the UTC month. Failed name-based lookups leave these values untouched; missing or invalid Name/Zip returns IncompleteAddress. A concurrent rename supersedes the result. No extra request or automatic retry is added.
The response contains kid, address, zip, nullable integer latitude, longitude, autoLatitudeLongitude, revision and outcome. Coordinates are millionths of degrees, not decimal degrees; latitude range ±90000000 and longitude ±180000000, with zero valid. Outcomes: Success, Unchanged, IncompleteAddress, ManualCoordinatesPreserved, ManualCoordinatesSaved, NoResult, TransientFailure, PermanentFailure, NotConfigured and Superseded. HTTP 200 can confirm a saved address even when geocoding fails; inspect outcome.
Errors use ProblemDetails with code: 400 invalid input/KID, 401 expired or changed session, 403 missing rights, 404 missing/invisible/deleted object, 409 stale revision (address-conflict), 503 storage unavailable. Reload on 409. After 503, timeout or a lost response, read back before retrying because the address may already be committed. The location overview has an address editor and a map above Units. Generated clients and downloadable packages are synchronized at version 0.3.2.
SetObjectCoordinateProvenance changes only AutoLatitudeLongitude: 0 (unknown), 1–12 (automatic lookup month), 13 (legacy pending), 20 (legacy failure), 30 (manual). Values 1–12 require a valid stored coordinate pair; 30 may be selected before coordinates exist. It returns ProvenanceSaved and never starts a lookup or schedules retries. canWrite is a display hint from the current permission snapshot; every write independently checks permissions.
The portal saves each address or postal-code field when it loses focus. Manual selects 30; Auto selects 13. Mode selection alone does not call Google. The map reads saved coordinates every ten seconds while the page is visible and refreshes if they change. These reads never initiate geocoding. Each address change allows at most one lookup when the source is not 30; success stores the UTC month. There are no background jobs or automatic retries after failure.
Windows app downloads
The primary page is https://{tenant}.kombine.technology/download; this API's download page serves the same content. Beta uses the matching beta hosts. No login is required to download; installing the app grants no business access. Sign in normally inside the app.
GetPortalAppDownloadPage: GET /download returns HTML in all ten portal languages according to Accept-Language (English default; Danish fallback for individual missing translations). DownloadPortalWindowsAppInstaller: GET /download/windows/{architecture}/portal.appinstaller downloads the installation/update file. DownloadPortalWindowsPackage: GET /download/windows/{architecture}/{fileName} serves its exact versioned MSIX. Architecture is x64 or arm64.
Open the downloaded file with Windows App Installer. The signed package must be trusted on the device; WebView2 Runtime and internet access are required. Update checks at launch depend on Windows and device policy. 404 means no validated release for this site's tenant/environment/architecture, or an unknown filename. The page then shows unavailable downloads; there is no cross-tenant fallback. Installer/page responses use no-store; MSIX supports byte ranges (206) and ETag (304). Client request parameters cannot select another tenant. Signed customer packages and deployment artifact delivery have not yet been configured.
Unit documents: tables, CSV, XLS and SVG
Use a canonical Doc KID containing the site's tenant, bank, location, unit and document TagId. A unit KID, numeric document ID or foreign tenant is not accepted. These operations read one known document; they do not discover document IDs.
Search documents
GetBankDocuments: GET /api/v1/banks/{bankKid}/documents discovers document KIDs for WashDoc1/2. It requires the same bearer, WashDoc Tab, Location Read, Unit Read and matching resource grants as the document endpoints. Only authorized, retention-visible locations and units appear in results and filter choices.
Optional locationKid and unitKid restrict the bank scope; a unit also selects its location. from/through are inclusive ISO 8601 timestamps with an explicit UTC offset, defaulting to the last 24 hours, with a maximum 31-day interval. Matching documents have a tagged Cycle setting inside that interval. lastActivityUtc means the latest matching Cycle, not the full start/end; the table provides full document bounds.
The JSON response contains items (kid, locationKid, unitKid, locationName, unitName, unitIconKid, unitType, lastActivityUtc), locations, units, effective from/through, offset, limit and hasMore. Units belong to the selected location scope. Read pages lazily with limit 1–100 (default 25) and offset 0–100000. Results sort by latest activity descending, then location/unit/document identity; live changes can shift pages, so reset offset to refresh. No total count or measurements are loaded by search.
400 means invalid filters; 401 sign in again; 403 no access; 404 selected object is hidden/missing; 422 narrow by location (at most 1000 locations, 5000 discoverable units, 35000 current-setting rows); 503 unavailable/busy, with no automatic retry loop. Searches have a 12-second storage deadline, two concurrent searches per API process and a one-second queue wait. Responses use no-store. In the portal, WashDoc1 and WashDoc2 share the search view, with 25 documents per page, a 200-row table preview and full CSV/XLS/print downloads. Search inputs and graph axes use UTC; list/table timestamps use browser-local display. Graph preview selects the first 16 fields with numeric values. Generated clients and downloadable packages are synchronized in version 0.3.2.
Operation
GET path
Output
GetUnitDocumentTable
/api/v1/documents/{documentKid}/table
JSON table and metadata
GetUnitDocumentHtml
/api/v1/documents/{documentKid}/table.html
Printable HTML table
DownloadUnitDocumentCsv
/api/v1/documents/{documentKid}/table.csv
UTF-8 CSV
DownloadUnitDocumentXls
/api/v1/documents/{documentKid}/table.xls
Excel 97–2003 binary workbook
GetUnitDocumentSvg
/api/v1/documents/{documentKid}/graph.svg
SVG charts
Access and fields
Send the manager bearer token on every request. Requires at least one of WashDoc1 (54), WashDoc3 (55), WashDoc2 (75), Location Read, Unit Read and a matching tenant/bank/location grant. Location and unit deletion follow RetentionDays. Authorization is reapplied before every SVG cache lookup, using the normal bounded manager/location/unit snapshots.
Optional states and settings are comma-separated exact enum names, for example states=Temperature,Level&settings=Cycle on a supported washer. Omit a category to select all its permitted fields; an empty value selects none. Only visible current-unit descriptor bindings are allowed. Credentials, hidden fields and fields belonging to other objects are never read. Unknown unit types return 422. Enum identifiers and export labels are language independent; presentation labels are currently English.
Values and downloads
JSON contains documentKid, unitKid, unitName, finished, document bounds in MS2000, columns and rows. Cells follow column order. A null cell means absent; a cell with null text/value is a stored null. text preserves the decoded original. value is a finite double when available; use text for exact large numbers. No endpoint interpolates or rounds table values.
CSV uses a UTF-8 BOM, commas, quoted cells and CRLF. Potential formulas in nonnumeric text receive a leading apostrophe. CSV cannot distinguish missing/null/empty cells. XLS is genuine BIFF8, with text written as strings rather than formulas; more than 15 digits and noncanonical numeric formatting stay as text. Both include MS2000 and ISO UTC columns. HTML can be printed using the browser. PDF is not provided.
Graphs and completed-data cache
SVG is rendered directly with .NET XML, with no third-party chart library, scripts or external assets. Each numeric series has a separate labelled scale. Enum/boolean values use steps; explicit null/invalid measurements break a line. Sparse timestamps from other series do not. Select at most 16 numeric series; width accepts 480–2400 (default 1200).
Only a document with a terminal Cycle and an InSync timestamp at least two minutes beyond its full end is finished=true. Only finished SVGs are cached, on private disk outside wwwroot, for at most 24 hours. Keys include tenant-bound KID, unit type/name, ordered fields, width and renderer version. The cache holds at most 128 MiB/256 files and falls back to fresh rendering on cache I/O failure. In-progress SVGs and all tables/downloads are uncached. All HTTP responses use Cache-Control: no-store. X-Document-Complete reports completion; that header and Content-Disposition are available to configured CORS clients. An SVG URL requires bearer authentication; fetch it before creating an image blob URL.
One document, up to 31 days, 128 columns, 50,000 source samples, 500,000 cells, 16,384 characters per value and 8 million characters overall. Storage has a 12-second deadline. At most two exports/renders run concurrently per API process, with a one-second queue wait. Failures never return a successful partial document.
400: invalid KID/field selection/width. 401: sign in again. 403: missing access. 404: missing or retention-hidden object/document. 422: unsupported unit type, document too large, no numeric graph data or more than 16 series; use fewer fields when applicable. 503: storage unavailable or document-busy; show unavailable and avoid automatic retry loops. Errors use ProblemDetails with a stable code. Generated clients and downloadable packages are synchronized in version 0.3.2.
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, resident editing, activation letters and CSV export are available through the operations documented below. Other Tabs may still lack business operations. 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.
Resident receipts — GetUserReceipts (unreleased)
GET /api/v1/users/{userKid}/receipts uses the existing manager bearer session. Requires an active account, Users2, User Read and access to the resident's entire bank. A location-only grant is insufficient. The canonical user KID must belong to this API's configured tenant. Permissions are checked on every page through the normal manager snapshot (at most 60 seconds old); missing or retention-hidden residents are indistinguishable.
API='https://localhost:20332'
USER_KID='A6Q3CoAC1b41Dh'
curl --get "$API/api/v1/users/$USER_KID/receipts" \
-H "Authorization: Bearer $TOKEN" --data-urlencode 'offset=0'
# For an older page, use nextOffset and revision from the previous response:
curl --get "$API/api/v1/users/$USER_KID/receipts" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "offset=$NEXT_OFFSET" --data-urlencode "revision=$REVISION"
The response contains userKid, revision, items[], nullable nextOffset and periodCount=24. Each page has at most 20 whole receipts; offsets count receipts, not lines. Start at zero without a revision. Stop at nextOffset:null. Load only when the receipt section or its end becomes visible, with one request in flight; do not poll the whole history.
Each receipt contains key, local date, nullable locationKid, locationName, period (zero is current), provisional, kind (Purchase, Payment, Discount, TransferToRent), nullable currency, totalMinor, nullable vatMinor, balanceAfterMinor and lines[]. Lines provide nullable transaction kid, offset-aware occurredAt, nullable unitKid, unitName, all description texts[], amountMinor and calculated. Calculated settlement adjustments have no stored transaction KID.
Money is signed 64-bit minor units. A negative stored posting becomes a positive receipt amount; refunds retain the opposite sign. Keep currencies separate without conversion; unknown currency stays null. Receipt balances exclude unused current-period discount, unlike the current balance header. Discount applies to DKK only. VAT is included in the total and is returned only for cash-bank purchases with a valid configured rate; null is unknown/not applicable. Groups are separated by local day, location, kind, period and currency. Zero-total documents and Month lines are omitted.
History covers period zero and the latest 24 stored period numbers, further limited by accounting/surveillance retention. Deleted-resident visibility follows the manager's RetentionDays. Each request builds a bounded read-only snapshot: 12-second storage deadline, two concurrent reads per API process, at most 50,000 postings, 8 MiB of stored text and 1,000 documents per receipt. SQL uses Log tables only. There are no refund, payment, export or mutation operations in this endpoint. Generated clients are updated at beta release.
Errors: 400 invalid-user/invalid-page; 401 (may have no JSON body); 403 forbidden; 404 not-found; 409 receipts-changed; 503 receipts-too-large/storage-busy/storage-timeout/storage-unavailable. A 409 means discard loaded pages and restart at zero: never combine different revisions. On 401/403/404 clear the history and stop loading. On 503 show an error and offer manual retry, never fabricated zero totals. No oversized or failed result is silently truncated.
Live logs
GetLiveLogs — GET /api/v1/diagnostics/live-logs uses your reusable manager bearer session. Requires an active manager, Logs1, Managers Read and a whole-tenant KID grant. A bank-only grant is insufficient. The site selects the tenant; no tenant or server selector is accepted.
The response contains tenantKid, fetchedAtUtc, refreshAfterSeconds and three servers cards: portal-api, equipment-api, portal-web. Each card has errorCode (null on success) and events, newest first. Events include timestamp, level, message template, category and optional statusCode, elapsedMilliseconds, bankId, userIds and traceId. Numeric bank/user IDs are diagnostic values, not resource selectors.
Poll no faster than every 3 seconds. Every request rechecks access before reading cached data. 401 means invalid/revoked session; 403 means missing-logs-tab, missing-managers-read or missing-tenant-access; 503 means authorization storage is unavailable. Stop polling and clear displayed data on 401/403. Per-card errors are live-logs-not-configured or live-logs-unavailable; other cards remain usable.
Each process retains at most 100 sanitized events for 15 minutes, cleared at restart. Application Information events and all Warning-or-higher events are captured. Message arguments and exception text are omitted. Empty cards can mean no recent events. This is in-memory diagnostics, not Logz.io history or an audit trail. Multiple replicas retain separate buffers; aggregation across replicas is not supported.
Chat question events additionally include optional question (up to 2000 characters). Authorized live-log viewers can see this text. Render it as plain text, never as HTML; the portal displays it in bold in place of the message placeholder.
Events also expose optional fields, a map of sanitized structured values. Substitute matching message placeholders as encoded text; the portal displays values in bold. Unknown or filtered placeholders remain unchanged. Existing event fields are retained.
When continuous DO-to-Logz.io forwarding is enabled, this endpoint reads the recent in-memory collector buffer. Delivery runs independently of open pages. A visible line confirms receipt from DigitalOcean, not acceptance by Logz.io. Logs1 also displays this panel; permissions remain unchanged. Forwarding is best effort, with possible gaps and duplicates after interruptions.
DigitalOcean runtime logs
GetHostingLogs: GET /api/v1/hosting/logs. Requires a manager bearer session, Logs1, Managers Read and whole-tenant access. Operator policy explicitly permits logs across tenants for the configured app. Hosting1 alone is insufficient.
environment: beta/production; application: portal-api/equipment-api/portal-web. Response: environment, application, fetchedAtUtc, lines (up to 100 plain-text lines), truncated. Poll at most every 10 seconds. Content is bounded to 256 KiB; successes and failures are cached for 10 seconds. Snapshots may overlap and are not a complete history. Render as text, never HTML. Pause freezes display while access checks continue; hiding/leaving clears data. No MySQL, build, deploy or crash logs.
400 invalid-hosting-selection; 401 invalid session; 403 missing-logs-tab, missing-managers-read or missing-tenant-access; 503 hosting-not-configured or hosting-unavailable. Clear on error and stop on 401/403. Provider credentials and signed URLs stay on the server. Access is rechecked before cache hits using the manager snapshot (up to 60 seconds old). Included in client packages 0.3.2; availability requires operator configuration.
Hosting1 — hosting metrics (unreleased)
GetHostingMetrics: GET /api/v1/hosting/metrics?environment=beta&application=portal-api&hours=24. Reuse the manager bearer session. Requires an active account, Hosting1 (81), Managers Read and a whole-tenant KID grant for the API site. Every call, including a metric cache hit, checks the manager snapshot, which is at most 60 seconds old. These are shared infrastructure measurements across customers, not the caller's individual tenant consumption.
environment: beta or production (default beta). application: portal-api, equipment-api, portal-web or mysql (default portal-api). hours: 1, 6 or 24 (default 24). Resource mappings are controlled by the API site; clients cannot supply a provider ID, tenant or URL. Beta and production can map to the same database.
The response contains environment, application, fromUtc, toUtc, fetchedAtUtc, refreshAfterSeconds, metrics, bandwidth. Each metric has name, unit, series, errorCode. Each series has component, instance, points; each point has timestampUtc and nullable numeric value. App metrics are cpu/memory in percent and restarts in count. Keep separate instances separate: restarts are the provider's series, not a calculated total for the selected window. MySQL returns cpu, memory, disk in percent using the provider's cluster average and connections (connected MySQL threads) in count. Missing provider labels use a numeric series label; it is display text, not a persistent object identifier.
For apps, bandwidth has dateUtc, bytes, errorCode. It always describes yesterday in UTC, independently of hours. Bytes is an unsigned decimal string, preserving 64-bit precision, or null when absent. For MySQL, bandwidth is null. An empty series or a null sample means missing data, never zero. Sample timestamps may lag; fetchedAtUtc identifies the snapshot, not measurement freshness.
Check every errorCode: individual failures return hosting-source-unavailable with no series (or null bytes); other measurements remain in HTTP 200. If all sources fail, the API returns 503 hosting-unavailable. Disabled or incomplete integration returns 503 hosting-not-configured. Other errors: 400 invalid-hosting-selection; 401 requires login; 403 missing-hosting-tab, missing-managers-read or missing-tenant-access. The listed 400/403/503 application errors use ProblemDetails with code. Malformed query types use standard model-validation errors; 401 can have an empty body. Provider error text and credentials are never returned.
Honor refreshAfterSeconds (60). Results and failed lookups are cached for one minute; browser responses are no-store. Pause polling while hidden, prevent overlapping requests, and clear displayed data after 401/403. Partial/empty data is not evidence of healthy infrastructure. Only the documented app and Managed MySQL metrics are available; the operation does not query SQL, change resources, expose logs or provide customer-specific billing. Generated clients and downloads will be synchronized at beta release.
TenantStatus1 — operational alerts
GetTenantStatus: GET /api/v1/tenant/status?limit=200. Reuse the manager bearer session. Requires TenantStatus1 (80), Bank Read, Location Read, Unit Read and a matching tenant/bank/location KID grant. Tenant navigation does not grant tenant-wide data access. The site selects the tenant; scope and RetentionDays for bank, location and unit are applied before limiting rows. Enabled must be exactly 1 at the location. Missing Deleted means zero; invalid or future deletions are hidden.
Offline uses the tenant Alive table (explicitly approved): UnitId = MainId, Cluster other than Test, contact between 100 days ago and one hour ago for Cash, or one day ago for other nonempty BankType values, including boundaries. AutoOutOfOrder uses Log24 OutOfOrder = AutoOutOfOrder, excludes StartSMS_60 and TimeSMS_61 and reads a positive integer error Id from the AutoOutOfOrder state JSON. UnitType2 takes precedence; otherwise the packed legacy type is (value >> 1) & 255. Missing/invalid types are excluded from that lookup.
Response: measuredAtUtc, refreshAfterSeconds (30), sources and items. Each source has kind, count, hasMore, errorCode. Each item has kind, kid, bankKid, locationKid, bankName, locationName, unitName, computerName, bankType, unitType, errorId, timestampUtc, iconKid, bankIconKid, locationIconKid. KIDs are canonical. unitType/errorId are nullable integers. timestampUtc means last contact for Offline or the OutOfOrder setting timestamp. Send Accept-Language for localized name placeholders. Items sort by timestamp descending, then kind and KID; a unit may have both alerts.
bankIconKid and locationIconKid contain the configured bank/location icons from Log24, with the object number encoded in Kid.Text by the API. Missing or invalid icon settings use bank_building/house. Render these values unchanged with GET /api/v1/icon/{iconSet}/{iconKid}.svg; for example, "/api/v1/icon/g/" + encodeURIComponent(item.locationIconKid) + ".svg". Use bankKid/locationKid for object links and workspace shortcuts. ComputerName, bankType and unitType remain available for supplementary tooltips; icons and shortcuts never grant access.
Lookups run in parallel on separate connections with a 12-second request deadline. A failed source returns status-source-unavailable, count 0 and no items while successful sources remain in HTTP 200. Always inspect sources; a partial result does not mean healthy. All sources failing returns 503 tenant-status-unavailable. 400 invalid-limit; 401 requires login; 403 codes: missing-status-tab, missing-bank-read, missing-location-read, missing-unit-read, missing-resource-access. Errors use ProblemDetails/code. Results are no-store.
Limits: 1–1000 rows per source, default 200. hasMore explicitly means only the newest rows are included; no cursor or historical export is provided. Accept additional kinds in future. Refresh no faster than every 30 seconds, stop while hidden and avoid overlapping requests. Generated clients and downloadable packages are synchronized in version 0.3.2.
const response = await fetch(apiBase + '/api/v1/tenant/status?limit=200', {
headers: { Authorization: 'Bearer ' + accessToken, 'Accept-Language': 'en-GB' }
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const status = await response.json();
for (const source of status.sources) {
if (source.errorCode || source.hasMore) console.warn(source.kind, source);
}
console.table(status.items);
Lazy pages — GetTenantStatusPage (unreleased)
GET /api/v1/tenant/status/page?pageSize=25 provides the same authorized alerts in pages of 1–100 rows (default 25). The existing GetTenantStatus contract is unchanged. The response has status (the status envelope above, with only this page in items), offset, totalCount, previousCursor, nextCursor. Source counts cover the bounded snapshot, not only the page. The API reads up to 1,000 rows per source once; hasMore still warns of truncation. Later pages reuse this snapshot without rerunning the lookups.
Send a returned cursor unchanged, keeping pageSize and Accept-Language unchanged. Every page rechecks the active manager, Tab and operation permissions. Cursors are bound to manager, tenant, credential stamp, resource scope, RetentionDays and language; they never grant access. Snapshots expire after two minutes and can be evicted sooner. HTTP 400 means invalid paging input/cursor; 409 means changed context; 410 means expired/evicted snapshot. For 409/410 discard the cursor and request a fresh page; 401/403/503 have the meanings above. Responses are no-store.
Refresh without a cursor every 30 seconds. Optional anchor=Offline:UNIT_KID starts at that authorized row; offset=50 (0–1999) is the clamped fallback if the anchor has disappeared. Do not combine cursor with anchor/offset. This allows refreshing around the visible row. The portal keeps at most five 25-row chunks and requests earlier/later pages when scrolling. No historical export or unlimited scan is provided.
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.
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.
Kombine logos — local SVG
Public, font-independent versions of the three original Kombine logos. Choose Public v1 (no login) in Swagger. No login, business KID, Tab, permission or database access is required. Rendering uses .NET XML, the original symbol geometry and locally embedded wordmark outlines, including the registered mark. No third-party graphics components, installed fonts, scripts or remote assets.
Artwork
Path prefix
Operation ID
Proportions
Symbol
/api/v1/logos/kombine
GetKombineLogo
1:1
Name only
/api/v1/logos/kombine-text
GetKombineText
590:111
Symbol and name
/api/v1/logos/kombine-logo-text
GetKombineLogoText
5:1
Each prefix has three forms: /{color}.svg, /{color}/{width}.svg and /{color}/{background}/{width}.svg. Operation IDs for the latter two append Sized and WithBackground. All parameters are path segments; no query string. SVG only, no raster-format fallback.
GET /api/v1/logos/kombine/black.svg
GET /api/v1/logos/kombine-text/black/512.svg
GET /api/v1/logos/kombine-logo-text/white/174d61/512.svg
Colors accept 3/6-digit RGB hex without #, exact eColor names, standard color names or transparent, case-insensitively. Unknown names are rejected, not guessed. Without a width segment, the SVG has no fixed width/height: its viewBox and preserveAspectRatio="xMidYMid meet" fit and center the entire logo in the available viewport without cropping or stretching. Explicit width is an integer from 16–4096 pixels; height follows the original proportions and can be fractional. Use a sized URL or dimensions on the embedding element for a fixed size. The background is transparent unless explicitly supplied. Unlike the old raster-only background handling, an explicit background is painted directly into the SVG. Include alternative text when embedding the image.
All inputs describe a complete image. First requests render and atomically save to private disk outside wwwroot; repeated variants reuse files, including after restart if the disk remains. The cache keeps at most 512 variants/32 MiB for 24 hours; keys distinguish artwork, normalized colors, width, renderer version and the embedded asset hash. Cache I/O failure falls back to fresh rendering. Responses use image/svg+xml, a public 600-second browser cache and ETag/If-None-Match with 304.
Errors: 400 for invalid colors or width, without silent clamping. Validated parameters return code: invalid-logo-parameters; malformed integers use standard validation ProblemDetails. 404 for unknown routes/formats such as PNG. 429 when 16 logo requests are already running; retry with backoff. These are new endpoints, not aliases of KombineLogo1/KombineText1/KombineLogoText1. Generated clients and downloadable packages are synchronized in version 0.3.2.
GetLinearGradient and GetLinearGradientSized render a linear gradient over the entire canvas. Select Public v1 (no login), tag Gradients, in Swagger. Public presentation only: no login, KID, permissions or database access.
colors: 2–4 hyphen-separated colors, evenly spaced; 3/6-digit RGB hex without #, exact eColor names, standard named colors or transparent (case-insensitive). angle: finite degrees clockwise; 0 = left to right, 90 = top to bottom, 180 = right to left, 270 = bottom to top. Use a dot for decimals. Negative angles and full turns normalize modulo 360. Dimensions default to 200 × 200 or accept 16–4096 pixels each. Geometry retains the angle at the requested dimensions and spans the full rectangle.
SVG only, all inputs in the path; no scripts or external assets. Supply alternative text when embedding. Private disk cache: 24 hours, at most 512 variants/32 MiB; I/O failures fall back to rendering. HTTP: image/svg+xml, public cache for 600 seconds, ETag/If-None-Match with 304. Errors: 400 ProblemDetails for invalid inputs (invalid-gradient-parameters for rendering validation; malformed numbers use standard validation), 404 for unsupported formats/routes, 429 when 16 concurrent gradient requests are active (retry with backoff). Generated clients and packages will be synchronized at the next beta release.
GET /api/v1/gradients/linear/{colors}/{angle}.svg
GET /api/v1/gradients/linear/{colors}/{angle}/{width}x{height}.svg
GET /api/v1/gradients/linear/22aa88-ffcc33/45/800x400.svg
curl --fail --output gradient.svg "$TENANT_API/api/v1/gradients/linear/22aa88-ffcc33/45/800x400.svg"
Circles and progress — SVG
Three public visuals replace the rendering functions of CircleGradient1, CircleProgress1 and CircleRunning1. Select Public v1 (no login) in Swagger. They render only the supplied colors and percentage, with no login, business KID, permission requirement or database call. They do not read equipment status or calculate progress. Rendering uses .NET XML and local geometry; no third-party graphics components, external assets or scripts.
Visual
Operation ID
With dimensions
Angular gradient background
GetCircleGradient
GetCircleGradientSized
Percentage circle
GetCircleProgress
GetCircleProgressSized
Running indicator
GetCircleRunning
GetCircleRunningSized
GET /api/v1/circles/gradient/{colors}.svg
GET /api/v1/circles/gradient/{colors}/{width}x{height}.svg
GET /api/v1/circles/progress/{background}/{colors}/{percent}.svg
GET /api/v1/circles/progress/{background}/{colors}/{percent}/{width}x{height}.svg
GET /api/v1/circles/running/{color}.svg
GET /api/v1/circles/running/{color}/{width}x{height}.svg
GET /api/v1/circles/gradient/22aa88-ffcc33-ee4444.svg
GET /api/v1/circles/progress/f0f4f3/22aa88-ffcc33-ee4444/65/200x200.svg
GET /api/v1/circles/running/22aa88.svg
colors is a hyphen-separated list of 2–4 colors; running uses one. Colors accept 3/6-digit RGB hex without #, exact eColor names, standard named colors or transparent, case-insensitively. No numeric enum indices or substring guesses. Dimensions default to 200 × 200, or accept 16–4096 pixels each. SVG only; all parameters are in the path, without a query string.
The gradient fills the rectangular viewport, matching the old angular background (starts at the bottom, clockwise). The progress disc stays circular: integer percent 0–100 draws that many square marks clockwise from the top. Colors interpolate over the full 100-percent scale. Zero has no marks; 100 has all 100. The running semicircle rotates once per five seconds using native CSS and stops for reduced-motion preferences. Supply alternative text when embedding images.
These images contain complete, fixed presentation data, so all three may be cached on private disk for 24 hours (maximum 512 variants/32 MiB). Cache keys include normalized colors, type, percentage, dimensions and renderer version; files are atomically written outside wwwroot. Repeated requests reuse disk files. Cache I/O failures fall back to rendering. Responses use image/svg+xml, a public 600-second browser cache and ETag/If-None-Match with 304. This is separate from document graphs, which still cache only completed documents.
Errors: 400 for invalid color lists, unknown colors, noninteger/out-of-range percentage or size (not silently clamped); validated rendering errors include code: invalid-circle-parameters. Malformed integers use standard validation ProblemDetails. 404 for unsupported routes/formats such as PNG. 429 when 16 simultaneous circle requests are already active; retry with backoff. New operations, not legacy URL aliases. Generated clients and downloadable packages are synchronized in version 0.3.2.
For location responses using house, the API fills IconKid.Text with the location's LocationId. g/house displays this text centered in black inside the house, fitting it to the available space. The icon test page accepts a text value for previewing; a bare house without location context has no number.
Presentation responses use iconKid (C#: IconKid), replacing icon. Related fields are bankIconKid and unitIconKid. Use these strings unchanged, URL-encoded, in /api/v1/icon/g/{kid}.svg. The API constructs the KID: an icon alone is its exact eIcon.ToString() name; additional text, count, colour or icons use canonical Kid.ToString(). Bank/location/unit numbers are already in Text. Calendar uses today's day of month (1–31) in Europe/Copenhagen, resolved before the URL is created. Refreshing metadata after midnight gives a new filename; image caching is unchanged.
GetIconPresentation — GET /api/v1/icon/presentation?iconKid=calendar returns {"iconKid":"..."}. Public presentation only: no login, database lookup, business grant or mutation. Optional text (up to 128 characters without controls), signed Int64 count and RGB color (0–16777215) replace those fields; other fields survive. Calendar always overrides text with today's day. Invalid parameters return 400; on network/503 failure keep the previous image and retry later. Metadata is no-store; do not change an existing image's cache rules.
Clients select permitted settings from availableIcons and still submit the enum name in icon-edit requests (icon or the Icon setting). Service responses additionally provide iconName for the selected setting. GetActiveLocationCount returns both count and the complete iconKid; do not assemble the badge in the client. An IconKid never authorizes access. See the changelog; generated clients and downloadable packages are synchronized in version 0.3.2.
GetIconAssetCatalog: GET /api/v1/icon/catalog/g returns a sorted JSON array of canonical eIcon names with a file directly in g (use line for that set). For a picker, intersect these names with availableIcons from the business response; missing assets should not be shown. No login or permissions are required to read the catalog, and it grants no write access. Rendering fallback from other sets is not included. Unknown sets return 404; retry 429/503 with backoff. The catalog is cached for ten minutes and reflects the deployed assets.
GetIconFromSet, GetIconImageFromSet and GetIconImageWithBackgroundFromSet render packaged local assets. No login, Tab, operation permission, database lookup or external icon server is required. Other KID fields do not select tenant data or grant permissions.
GET /api/v1/icon/{iconSet}/{kid}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{backColor}/{size}.{format}
GET /api/v1/icon/line/413132x7qE20i11Bi336699Ic.svg
GET /api/v1/icon/g/413132xE20i11Bi336699Ic/128.png
GET /api/v1/icon/line/house/white/128.jpg
kid is a canonical Kombine.Flex.Kid.ToString() value. The API reads Kid.Icons, Kid.Count (Int64), Kid.Color and Kid.Text. For example, Kid.Icons = [house, check], Count = 7, Color = 0x336699, Text = "A12" produces 413132x7qE20i11Bi336699Ic. If canonical KID parsing fails, an exact case-insensitive eIcon name is accepted with count zero, black and empty text; house and HOUSE work. Numeric enum IDs, indices and substring matches are not name fallbacks. Unknown names, malformed/noncanonical KIDs and undefined icons return 400. A valid KID without an explicit icon uses eIcon.none.
The separate count, color, text and sub segments and all routes without iconSet are removed. Choose line or g. Kid.Color uses the low 24 bits as opaque RGB: 0 is black, 0xFFFFFF is white, and the high byte is ignored to preserve Flex eColor semantics (ARGB values produce the same RGB). Kid.Text is case-sensitive UTF-8 text, up to 128 characters without controls, used only by assets with a text box. The canonical KID may contain up to 2048 characters after text encoding; URL-encode it as one path segment. Count ≤ 0 hides the badge. Positive counts display in full in a red capsule with circular ends and a straight middle. The badge widens for extra digits without stretching its end caps or changing its height/font size; very long labels widen the SVG canvas. The SVG title also retains the full count. The first entry, Kid.Icons[0] (also exposed as Kid.Icon), selects the main icon; Kid.Icons[1] selects the under-icon. A missing second entry or eIcon.none omits the under-icon. An empty list uses eIcon.none as the main icon. Further entries are ignored for rendering, but all entries must be defined eIcon values. Other parameters remain limited to 128 characters. No query parameters are used.
Formats: svg, png, jpg/jpeg, gif, bmp, tif/tiff, webp, ppm, tga, ico. Size is clamped to 16–4096 pixels (ICO at most 256). SVG retains its authored viewport; size/background affect raster images. Omit the background segment for transparent raster output. Unknown formats return SVG with image/svg+xml, matching the existing endpoint. Raster notification labels use the bundled Noto Sans Bold font.
On the first request the image is rendered and saved to local disk; the same variant is then streamed from disk, including after a restart if that disk is retained. Old cache entries can be evicted; replacement hosts/deployments may have an empty cache. Responses use a ten-minute public browser cache and support ETag/Last-Modified conditional requests with 304. Handle 400 for invalid/oversized parameters, 404 for missing local assets, 429 for concurrency limits and 503 for unavailable rendering/storage; retry transient errors with backoff. No database fallback or public cache-clear route is provided.
Icon sets:g preserves the original multicolor SVGs; line uses its palette/text annotations. Main and under-icon each try the selected set first, then the other packaged sets in ordinal alphabetical order for the same identity. They may come from different sets. Unknown sets or assets absent from every local set return 404. Catalogs list only direct membership, and disk-cache identity includes the selected set. For example, /api/v1/icon/g/jaa_ckey.svg uses the line asset because g does not contain jaa_ckey.
Migration: add the main icon and optional under-icon to Kid.Icons in that order, set Count, Color and Text on the Kid, URL-encode ToString(), select the set and remove the count/color/text/sub segments. An exact eIcon name still works for black icons without text or badges. Old paths are not compatibility aliases; rebuild stored URLs and follow the public changelog. Generated clients and downloadable packages are synchronized in version 0.3.2.
Daily cleanup removes rendered variants unused for 100 days by default. The API tracks use independently of filesystem access times. Capacity limits can evict variants sooner; a later request regenerates them from the original bundled assets.
Services — fixed service identities
GetServices: GET /api/v1/services lists the concrete service values of eUserId, including those without saved settings. Range-end markers are excluded. Each item has a canonical kid, enum identity (ToString), name and iconKid. Only Name and Icon are selected from this site's A{TenantId:D4}.Log7, BankId 0. The finite catalog is returned in one response; nextCursor is null. Optional filter is a literal case-insensitive match on identity or name (maximum 128 characters); sort=identity|name, direction=asc|desc. Identity sorting uses its numeric enum value.
GetService: GET /api/v1/services/{serviceKid} returns service, hasApiKeyHash, apiKeyHash, canEdit, profileRevision and availableIcons. A service KID uses the existing Manager type, this site's tenant, bank zero and a concrete service ID. Do not construct a human manager KID or infer permissions from its type.
Permissions and editing
All calls require an active manager session, Services1 (60), independent PermissionService2.Read and a whole-tenant KID grant. Editing additionally requires Service Write. Readers receive a null apiKeyHash; only editors receive the stored hash. Treat it as sensitive and do not log it. Hashes never appear in directory responses.
SetServiceProfileField: POST /api/v1/services/{serviceKid}/profile/{field} saves exactly one field: Name or Icon. Name allows up to 200 non-control characters. Icon must be a selectable name from availableIcons (all known positive eIcon values); a safe legacy current icon is prepended for display only. ApiKeyHash is read-only: attempting to write or clear it returns HTTP 400 invalid-service-profile, including when the value is empty. Use GenerateServiceApiKey below to replace the API key; never submit a plaintext key or a manually computed hash. Existing stored hashes remain unchanged until a new key is generated. See the Changelog for migration details.
Each write rechecks the actor's current account, credential stamp, tab, rights and scope inside a serializable transaction. One setting is appended to bank-zero Log7 history; unchanged values add no history. Wait for the complete 200 response before updating the UI and retain the returned revision. No automatic write retries. Responses are no-store; requests have a 12-second deadline and writes allow a maximum 4096-byte body.
const headers = { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' };
const listResponse = await fetch(`${api}/api/v1/services?sort=name&direction=asc`, { headers });
if (!listResponse.ok) throw new Error(`GetServices: ${listResponse.status}`);
const { items } = await listResponse.json();
const serviceKid = items[0]?.kid;
if (!serviceKid) throw new Error('No services');
const detailResponse = await fetch(`${api}/api/v1/services/${serviceKid}`, { headers });
if (!detailResponse.ok) throw new Error(`GetService: ${detailResponse.status}`);
const detail = await detailResponse.json();
if (!detail.canEdit) throw new Error('Service Write is required');
const saved = await fetch(`${api}/api/v1/services/${serviceKid}/profile/Name`, {
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ value: 'Scheduled integration', expectedRevision: detail.profileRevision })
});
if (!saved.ok) throw new Error(`SetServiceProfileField: ${saved.status}; reload before retry`);
const confirmed = await saved.json();
Errors: 400 invalid-filter/invalid-sort/invalid-service-kid/invalid-service-profile — correct the request. 401 — log in again. 403 missing-services-tab/missing-services-read/missing-services-write/missing-tenant-access — request the missing access. 409 service-profile-conflict — reread and review changes. 503 services-unavailable — reread before a manual retry; a lost acknowledgement can have an uncertain outcome. Do not display a failed save as successful.
Generate a new API key
GenerateServiceApiKey: POST /api/v1/services/{serviceKid}/api-key accepts {"expectedRevision":"<profileRevision from GetService>"}. It requires the same Service Read/Write, Services1 and whole-tenant access as profile edits, including fresh authorization inside the transaction.
The API generates kt_ followed by 64 cryptographically random ASCII letters and digits, always including both uppercase and lowercase letters. Keys are case-sensitive. It stores only the hash in eSetting.Password, using exactly the existing manager password function (HubManager.SHA512Salt compatible). This replaces the previous hash. The successful response contains apiKey and details, including the committed hash and new revision. The plaintext key is returned only by this call and cannot be recovered through GetService. Show/copy it only after a confirmed response; do not log the response or put the key in browser storage, URLs or analytics. The portal clears its displayed key on navigation or replacement.
Service key hashes are stored in eSetting.Password, the same Log7 setting as manager passwords. eSetting.ApiKeyHash is no longer read or written. Before switching an installation to this version, move any existing service hashes to Password through the Log7 history mechanism, without overwriting an existing Password, or generate replacement keys in Portal. Existing plaintext keys continue to work if their hashes are moved unchanged. There is no automatic migration or fallback. The JSON fields apiKeyHash and hasApiKeyHash keep their names and describe Password; manual credential editing remains prohibited.
Uses the same 400/401/403/409/503 errors, no-store, 12-second deadline and 4096-byte request limit as profile editing. After a timeout or lost response, reread before manually generating again: the previous request may have committed. Never retry automatically. This does not enable service authentication. Generated clients and downloadable packages are synchronized in version 0.3.2.
In Swagger, choose Downloads v1 under Select a definition for resident CSV, account CSV/Excel, settlement ZIP and document CSV/XLS exports. Use the same manager bearer token and permissions. The complete integration OpenAPI document still includes these operations for client tools.
Managers may share an email address if their passwords differ. If several accounts match both email and password, login keeps the active, non-deleted manager with the newest eSetting.Alive krumb MS2000 and atomically clears Password on the other matching managers. Activity comes from the krumb timestamp, never its Text value. Missing or invalid timestamps rank behind valid activity; equal timestamps, including all unknown, are resolved by the lowest UserId. Credentials and activity are re-read under transaction locks before cleanup. Accounts with a different password remain unchanged; permissions are never merged. Without an active matching account, no cleanup occurs. Read GetCurrentManager after login to identify the selected account.
No token is issued until duplicate cleanup commits. Unavailable storage, failed cleanup or more than 100 matching rows return HTTP 503. Do not automatically retry an uncertain result. Sessions belonging to accounts whose passwords were cleared become invalid; other API instances may retain their existing account snapshot for up to 60 seconds. Request and token response fields are unchanged.
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 accessToken value from the response, without quotation marks. Click Authorize, paste it under ManagerBearer, and authorize. Swagger adds the Bearer prefix.
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.
GET https://api.team.kombine.technology/api/v1/session/me
Authorization: Bearer <your access token>
Accept: application/json
The token lasts three days (259,200 seconds) from login or renewal. Treat it as a secret string: it is not a JWT to decode. Read expiresIn instead of hard-coding the duration. Before expiry, replace it through RenewManagerSession after user activity; ordinary API calls do not extend it. An expired token requires login. No separate refresh token or single-token revocation endpoint is provided. On logout, your client discards its token; an existing copy retains its original expiry, subject to account/password revocation.
No request body is required. Replace the stored token and deadline only after success. The API rechecks the same site, active account and password stamp through its bounded snapshot (at most 60 seconds); renewal adds no Tab, KID or operation permissions. Local-development sessions remain restricted to Development and direct loopback connections. HTTP 401 requires login; 429 requires waiting for Retry-After; 503 means account storage is unavailable. A failed renewal does not extend the old token. Avoid parallel renewals and ignore late responses belonging to an earlier login.
The official portal uses a persistent, protected cookie and renews both cookie and API token after visible-page mouse, keyboard or scroll activity, at most once per minute. An idle open tab, public statistics and background requests do not renew it. Logout removes the browser cookie. Existing one-hour sessions keep their old deadline; log in again to start the new three-day portal session. Generated clients and downloadable packages are synchronized in version 0.3.2. Packaged clients do not renew automatically.
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.
GetMyManagerProfile and GetMyManagerTabs use the site's read connection and work without a configured write connection or MySQL write privileges. canEdit describes the manager's authorization, not the database account's privileges. Saving still requires a configured write connection with the necessary database permissions; unavailable writes return 503 and must not be retried automatically.
The portal opens /my-settings from the signed-in manager icon. Integrations use the same public operations below. Except for redeeming an emailed confirmation token, all require an active, tenant-bound manager bearer session with a current credential stamp. They operate exclusively on that session's manager. Profile, email and password operations need no business Tabs, Kids or Managers Write. Changing your own tabs requires an explicit all-banks KID grant as described below. None of these operations change KID scopes, operation permissions, account state or other users. Administrative self-edit restrictions on the manager directory remain in force.
Operation ID
HTTP
GetMyManagerProfile
GET /api/v1/session/me/profile
SetMyManagerProfileField
POST /api/v1/session/me/profile/{field}
GetMyManagerTabs
GET /api/v1/session/me/tabs
SetMyManagerTab
POST /api/v1/session/me/tabs/{tabId}
RequestMyManagerEmailVerification
POST /api/v1/session/me/email-verification
ConfirmMyManagerEmail
POST /api/v1/session/me/email-confirmation
ChangeMyManagerPassword
POST /api/v1/session/me/password
Profile, icon and theme
The profile response contains kid, name, organisation, iconKid, email, emailVerified, themeMode, iconSet, retentionDays, revision, availableIcons. No password hash or internal proof is exposed. The icon list contains person icons, with an existing non-person icon first when necessary. Writes accept only the exact field names Name, Organisation, Icon, ThemeMode, IconSet and RetentionDays. Name/organisation accept strings up to 200 characters without controls; new icons must belong to the person catalog; themeMode is numeric System=0, Light=1, Dark=2. RetentionDays requires a JSON integer from 0 to 2147483647: days you may see deleted records within your existing access. 0 hides deleted records. This changes visibility, not physical deletion; Tabs, Kids and operation permissions still apply. The response field is retentionDays; missing or invalid stored values return 0.
IconSet stores your personal icon-set preference as the exact JSON string "g" or "line". Both GetMyManagerProfile and GetCurrentManager return iconSet; missing or unsupported stored values return g. The portal applies this choice to navigation, headings, lists and saved workspace links. Use the unchanged API-provided IconKid in /api/v1/icon/{iconSet}/{kid}.svg; the existing cross-set fallback still applies when an asset is missing. This preference requires only your active own-account session and does not grant business access.
POST /api/v1/session/me/profile/IconSet
Authorization: Bearer YOUR_MANAGER_TOKEN
Content-Type: application/json
{"revision":"REVISION_FROM_GET_MY_MANAGER_PROFILE","value":"line"}
Use the returned revision for the next save. Unsupported values or JSON types return 400; a stale revision returns 409: reread before retrying manually. Inactive sessions return 401; on 503 or a network failure keep the last confirmed preference and reread before retrying an uncertain write. Changing this preference does not change icon identities or image cache rules.
Send the last acknowledged revision with one value. A successful write returns the complete acknowledged profile and a new revision; do not mark a selection saved before this response. HTTP 409 profile-conflict requires a fresh read and a user decision before overwriting. The existing SetCurrentManagerTheme operation also remains available.
GetMyManagerTabs returns kid, tabs, availableTabs, revision, canEdit; each tab has a stable numeric id and enum-derived name. Any active manager can read their own selection. canEdit requires an explicit grant for the site's whole tenant (all banks and locations). Individual bank/location grants do not qualify, even if they cover every current bank. No Managers tab or Managers Write is required. Tabs do not grant operation permissions, and unimplemented tabs remain hidden from the workspace.
Choose a tabId from availableTabs, then send only boolean enabled and the last tab revision (64 hexadecimal characters). This revision is separate from the profile revision. The session identifies the manager; no manager/tenant selector is accepted. The current credential and all-banks grant are rechecked inside the Log7 transaction. Unknown numeric stored tabs are preserved, and unchanged selections create no history. Any own tab, including Managers, can be removed and restored while all-banks access remains.
GET /api/v1/session/me/tabs
Authorization: Bearer ACCESS_TOKEN
POST /api/v1/session/me/tabs/60
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"revision":"REVISION_FROM_GET_MY_MANAGER_TABS","enabled":true}
HTTP 200 returns the complete acknowledged tab response. HTTP 400 means an invalid request/tab; 401 means an invalid or revoked session; 403 missing-tenant-access means the all-banks grant is absent; 409 tabs-conflict or invalid-stored-tabs requires a fresh read and a user decision. A network error or 503 can have an uncertain outcome: never retry automatically. This instance invalidates its permission cache immediately; other instances refresh within 60 seconds. The portal refreshes workspace tabs immediately after the confirmed save, including open bank branches, without replacing personal-setting drafts. If navigation cannot be refreshed, the saved selection is retained and a Reload link is shown.
Verified email changes
Request verification with the new email and the current password, including for local development sessions. Optional language is en (default), da or es. HTTP 202 email-verification-queued confirms the queue commit, not delivery. The existing sign-in address remains unchanged until an explicit confirmation POST. The same flow can verify an existing address.
POST /api/v1/session/me/email-verification
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"email":"[email protected]","currentPassword":"CURRENT_PASSWORD","language":"en"}
POST /api/v1/session/me/email-confirmation
Content-Type: application/json
{"token":"TOKEN_FROM_EMAIL_FRAGMENT"}
The trusted portal link opens /verify-email#token=…, expires after 30 minutes and is single-use. No request may supply a return URL or target identity. The portal removes the fragment and submits the token through its CSRF-protected form; GET requests never mutate accounts. Confirmation requires no bearer, since the protected token grants only that exact address change. It is tenant/environment-bound and tied to email, password and confirmation log versions. Changes to these values invalidate outstanding proofs, even if a previous value is later restored. Among concurrent proofs, the first successful confirmation invalidates the rest.
HTTP 200 email-verified atomically saves the address and its version-bound proof, and queues a notification to the former valid address. emailVerified becomes false if an administrator later changes the address; old or manually assigned flags are not treated as this proof. Existing sessions keep their current expiry and permissions; use the verified address on the next login.
Development delivery restriction: only exact domains nortec.dk, kombinetech.com and arendt.dk can currently receive a verification link. Other addresses return 400 email-delivery-restricted without changing the account or queuing a proof. Mail redirected to the development inbox cannot prove ownership of another address. Notifications still follow the central delivery policy: other recipients go to [email protected], with no Cc/Bcc.
Password changes
POST /api/v1/session/me/password
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"currentPassword":"CURRENT_PASSWORD","password":"NEW_PASSWORD","confirmPassword":"NEW_PASSWORD","language":"en"}
Supply the current password and matching new entries. New passwords must differ from the current password and contain 12–128 printable ASCII characters without leading/trailing spaces, preserving shared legacy login compatibility. HTTP 200 password-changed commits password history and notification mail together. Discard the old bearer and sign in again. The portal signs out immediately after success; other API instances can cache the previous credentials for up to 60 seconds. This is password reauthentication and confirmation, not two-factor login.
Errors and integration limits
400 codes include invalid-profile, invalid-email, email-already-verified, current-password-invalid, invalid-password, password-unchanged and invalid-email-token. An invalid email token may be expired, used, stale, from another site, or bound to an inactive account. Framework validation failures use ValidationProblemDetails; unknown request members are rejected. 401 requires fresh login; 409 requires a profile reload; 429 means a rate limit; 503 account-unavailable means unavailable or uncertain storage.
Email requests and password changes each allow two attempts per account per 15 minutes per API instance, plus the shared recovery IP limit (10/minute). Confirmation uses the IP limit. All writes recheck the current account inside the storage transaction, with a 12-second deadline. All responses use no-store. Never log passwords, full request bodies or tokens. Never automatically retry an uncertain mutation; first check your profile or try normal login. Mail delivery depends on the existing asynchronous worker. Generated clients and downloadable packages are synchronized in version 0.3.2. These endpoints are new; no existing contract was renamed.
Forgotten passwords
RequestManagerPasswordReset and ResetManagerPassword are anonymous HTTPS operations. The API host selects the tenant. Recovery works only for one unambiguous, active manager account; it never grants Tabs, Kids or operation permissions.
Temporary development delivery restriction: a single plain email address on exactly nortec.dk, kombinetech.com or arendt.dk receives directly. All other recipients are replaced with [email protected]. Domain matching is case-insensitive; subdomains and recipient lists are not allowed through. Cc and Bcc are always cleared. The submitted email still identifies the original manager. This restriction applies in all environments and has no configuration override.
POST /api/v1/session/forgot-password
Content-Type: application/json
{"email":"[email protected]","language":"en"}
The response is HTTP 202 {"code":"accepted"} for existing, unknown, ambiguous, disabled, deleted and per-address throttled accounts. There is no token in the response. The email contains a single-use link that expires after 30 minutes. Language can be en (default), da or es. Delivery is asynchronous; acceptance is not a delivery receipt.
POST /api/v1/session/reset-password
Content-Type: application/json
{"token":"TOKEN_FROM_EMAIL","password":"A new example password!","confirmPassword":"A new example password!"}
Use 12–128 printable ASCII characters with no leading/trailing spaces, and a password different from the current one. This restriction avoids silent character replacement by the existing shared password hash. Passwords remain compatible with normal login. HTTP 200 {"code":"password-reset"} confirms the password history and notification were committed together. Sign in normally; recovery does not automatically create a session. Other API instances may retain a previously valid session snapshot for up to one minute.
HTTP 400 invalid-password means a policy/confirmation failure; invalid-reset covers expired, already used, wrong-tenant, changed-account or invalid links, an inactive account, and an unchanged password. Request a new link when needed. Invalid JSON/field validation also returns 400 validation details. HTTP 429 includes Retry-After; HTTP 503 means configuration/storage is unavailable. Do not automatically retry an uncertain password write: try signing in or request another link. Never log tokens or request bodies.
The portal reads the email token from a URL fragment into a CSRF-protected POST form and removes the fragment from the address bar. Reset-page responses use no-store and no-referrer. Generated clients and downloadable packages are synchronized in version 0.3.2.
Manager invitations
InviteManager sends an existing manager an invitation to choose a password. Use an active manager bearer session with Managers1 (28), Managers Read and Write, and tenant-wide access. The own-profile lock also applies: editing your own account requires being the sole active manager with tenant-wide access. The API rechecks current credentials, permissions and target visibility inside the queue transaction.
POST /api/v1/managers/{managerKid}/invitation
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"expectedRevision":"PROFILE_REVISION_FROM_GET_MANAGER","language":"en"}
Replace expectedRevision with the 64-character profileRevision returned by GetManager or the latest confirmed profile save. The target must be enabled, not deleted and have a valid saved email address. The request cannot override the recipient or portal URL. Language is en (default), da or es.
HTTP 202 {"code":"invitation-queued"} confirms the mail queue commit, not delivery. The same recipient restriction applies to invitations: only nortec.dk, kombinetech.com and arendt.dk receive directly; every other recipient goes to [email protected], with Cc/Bcc cleared. Sending does not change the password, account state, Tabs, Kids or permissions. The link opens /accept-invitation, expires after 30 minutes and uses ResetManagerPassword with the password policy described above. Once the password is set, the link cannot be reused; normal login is required.
HTTP 400 means an invalid KID/request; 401 requires a new session; 403 means insufficient access or a locked own profile; 404 includes retention-hidden targets. HTTP 409 profile-conflict requires reloading the manager, while manager-invitation-invalid requires correcting the saved email or account state. HTTP 429 invitation-rate limits invitations to two per target per 15 minutes per API instance; follow Retry-After. HTTP 503 manager-invitation-unavailable indicates configuration/storage failure. Do not automatically retry uncertain responses because mail may already have been queued. Generated clients and downloadable packages are synchronized in version 0.3.2.
Client IP for server-side login
The API records the connection's IP address for rejected login attempts. A portal calling the API from its own server can additionally send its browser client's IPv4/IPv6 address in the optional X-Portal-Login-Client-IP header:
X-Portal-Login-Client-IP: 192.0.3.20
Send only an address observed by the portal server itself. The API treats this header as caller-reported metadata and records it separately from its own observed IP. It grants no permissions and does not change tenant binding, login restrictions or rate limits. Invalid or multiple addresses are ignored without changing the login response. Direct API clients do not need this header. Login responses and error handling remain unchanged; no diagnostics are returned in the response.
2. Understand the profile and permissions
This response is fictitious. Always use the actual values returned by the API.
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, iconKid
Display name and icon identifier. The name can be empty. house can be displayed from /api/v1/icon/g/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.
Portal database access
GetCurrentManager includes databaseAccess in the same profile request. Reuse the manager bearer session; no tenant or bank selector is accepted. This field is for display, for example in the workspace footer. canWrite=true means that the read/append privilege check on the tenant's shared Log7 and bank-zero Log7 succeeded. false means reading succeeds but the portal write connection is missing, denies privileges, or the server is read-only. null or an absent field means unknown, never confirmed read-only access.
checkedAtUtc is the UTC start of the check. Known results are shared for 10 minutes per tenant/API instance; failures for one minute. There are no test writes or background polling. A failed check does not turn an otherwise valid profile response into a login failure. Existing session 401/503 rules still apply. This is an indicative check: privileges may differ between banks, and triggers/transactions are not checked. Administrator exceptions to read-only are not assumed. The field never grants access; operations still require the manager's account, Tab, KID and operation permissions.
Save your theme
GetCurrentManager returns themeMode, a numeric eThemeMode: System=0, Light=1, Dark=2. Missing or invalid stored values default to System. Apply this value when signing in, including on a different browser or device. System follows the device appearance; do not save the currently resolved light/dark color as the preference.
SetCurrentManagerTheme saves only the authenticated manager's own setting. It requires an active site-bound manager session, including the current account state and credential stamp. It does not require bank Tabs, resource grants or resident-write permission and does not grant business access. No tenant, bank, manager ID or KID override is accepted.
POST https://api.team.kombine.technology/api/v1/session/me/theme
Authorization: Bearer <accessToken>
Content-Type: application/json
{"themeMode":2}
Success is HTTP 200 with {"themeMode":2}. Repeating the same stored value does not create another history record. The saved value is included in the next profile read; the writing API instance invalidates its profile cache. Other instances can retain an older profile for up to 60 seconds. Responses are no-store.
Missing/unknown modes or extra request fields return 400; expired/revoked sessions return 401. Unavailable persistence, including an unconfigured write connection, returns 503. Keep the last confirmed preference and explain a failed save; do not claim that a local browser preview was saved. After an uncertain network result, re-read the profile before retrying. There is no background preference polling or cross-device push; a later login loads the stored setting. The operation uses the same HTTPS/JSON bearer contract for official portals, external portals and authorized agents.
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?
Installer comes from eSetting.PermissionInstaller2 (3016), read from current bank-zero Log7. It is included in GetCurrentManager, GetManagers and GetManager. GetInstallers requires Installer Read, Installers1 and tenant-wide access; these flags do not grant permissions in other categories. Match permission entries by resource, not array position or a fixed category count.
The seven categories Managers, Bank, Location, Unit, User, Installer and Service use independent ePermission2 bits from PermissionManagers2/Bank2/Location2/Unit2/User2/Installer2/Service2. flags is the bitmask: Read=1, Write=2, Create=4, Delete=8, RenameExtrenatId=16, Rename=32. For example, 5 grants reading and creation, but neither writing nor deletion. No flag implies another.
Missing/empty values default to Read (1); explicit 0 grants nothing. Invalid values or unknown bits yield null and no access. Use canRead, canWrite, canCreate, canDelete, canRenameExternalId and canRename for display. The retained level field is enum text and may be numeric for combinations. Old Permission settings are not used. Tabs, KID scope and active account checks remain required; insufficient rights return HTTP 403. Managers flags do not introduce new manager operations.
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.
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.
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 session operation names are LoginManager, RenewManagerSession and GetCurrentManager; GetPortalStatus remains public. The host application handles login, token replacement and expiry. Sessions last three days and can be renewed before expiry after user activity. There are no machine accounts, API keys, delegated OAuth or MCP server yet; unattended access needs a separate authentication design.
Read-only assistant on the search page
AskPortalAssistant — POST /api/v1/assistant/query accepts question (1–2000 characters) and optional history (up to 12 messages with role user/assistant and content; 8000 characters per message, 20000 total). The response has plain-text answer, attempted business operations and up to 20 links with canonical kid and relative portal path. Treat generated answers as assistance and verify important facts in the portal.
The assistant discovers eight approved read operations from OpenAPI: SearchBanks, SearchLocations, SearchUsers, GetSearchBank, GetBankLocations, GetLocations, GetLocationUnits and GetTenantStatus. Each read uses your manager bearer session and the existing Tab, KID scope and Read permissions. No additional access or writes. Questions, supplied history and relevant authorized results are sent to OpenAI; credentials are never model input. The current question text is logged once to Logz.io after session revalidation when shipping is enabled, before model use. History and answers are not included in that event. Delivery is best-effort; retention follows the Logz.io account settings. No server-side chat history is retained; store=false disables Responses application-state storage, not necessarily all provider retention.
The assistant uses reviewed instructions packaged with the API and can load three named skills: offline-installations, find-location and explain-balance. Skills grant no additional operations or permissions. The balance skill explains limitations: the chat catalogue currently contains no balance or account-transaction reads. Skill loading counts toward the seven model turns, not the eight business reads.
curl "$BASE/api/v1/assistant/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept-Language: en-GB" \
--data '{"question":"Which of my sites are offline?","history":[]}'
Limits: eight business reads, seven model turns, 120 seconds, paged reads at most 50 rows and 48000 bytes per tool result. Non-paged overview endpoints retain their existing bounds. Oversized results are withheld as truncated; failed sources and hasMore are not proof of complete or healthy data. 400: invalid question/history. 401/403: session unavailable; sign in again or check permissions. 429: wait for Retry-After (six questions/minute per manager, four simultaneous requests per process). 503 with code=assistant-not-configured: ask the host administrator to activate it; other 503 errors indicate provider/network failure or timeout. Retry manually. This operation is server-to-server; it has no browser CORS policy. Existing session, error and language conventions apply. Generated clients and downloadable packages are synchronized at version 0.3.2.
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 the guides are included in published API output and controlled by ApiDocumentation:Enabled (default true). English is the primary documentation language at /docs (also /docs/en), with Danish at /docs/da and Spanish at /docs/es. Client packages use an English README.md and include README.da.md and README.es.md. 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 iconKid; 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 /api/v1/icon/g/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.
iconKid 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 /api/v1/icon/g/{iconKid}.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.
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), email (2800), 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. Email is read from the current Log7 eSetting.Email value; both JSON strings and older plain text are decoded. This field is read-only and cannot be changed through profile.
sms is the resident’s current phone number from Log7 eSetting.SMS, decoded from a JSON string or legacy text without changing its formatting. Missing values are empty; older data may use 0 for no number. It is read with the other details under the same tab, read, resource and retention rules. This read-only field does not send texts or place calls. The portal uses mailto: and tel: for valid contact values; invalid values remain plain text. Link handling depends on the device’s mail and phone apps.
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, email, 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 iconKid, 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,iconKid,enabled,deleted,deletedAt}. Use a plain bank KID and your manager bearer token.
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. An explicit site-wide grant shows all location states, including disabled locations and deletions outside RetentionDays. Other grants show only Enabled exactly 1 and non-deleted locations or deletions within RetentionDays. Zero retention hides all deleted locations; malformed and future deletion values are hidden for limited grants. 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, Deleted or Enabled setting in Log24 are discoverable. Missing or invalid icons default to house. Use kid as the location ID and name as its display name.
Status fields: enabled is true only for stored Enabled exactly 1; missing or invalid values mean false. deleted is true for a positive Deleted MS2000 value, false for zero (including missing), or null for malformed values. deletedAt is the UTC deletion timestamp, or null for zero or an unrepresentable value. Display inactive when enabled is false, otherwise deleted when deleted is true, otherwise active. Status never grants access; visibility is filtered by the API using the current manager scope and RetentionDays. These fields match GetLocations and come from the same cached Log24 read.
Location overview and units
GET /api/v1/locations/{locationKid}/units, operationId GetLocationUnits. Returns {location:{kid,name,iconKid},items:[{kid,name,iconKid,cycle,cycleText,unitType,unitTypeName,unitTypeSource}]}. Use a canonical location KID from the bank location list or a user's locations; user location entries now also include the location name.
Requires an active manager, at least one Tab, Location Read and Unit Read (Read must be explicitly set), 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.
Unit data from Log24 is cached for up to 10 seconds. Units need Name, Icon, Deleted, Cycle, UnitType or UnitType2 to be discoverable. Missing names are empty; missing/invalid unit icons use object_cube. Valid configured icons are preserved. The API includes the unit number in IconKid.Text; g/object_cube and line/object_cube display this text on the box face. The same default applies to unit overviews, documents, bookings and account entries. 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.
Location opening hours
GET /api/v1/locations/{locationKid}/opening-hours, operationId GetLocationOpeningHours, returns the effective schedules of the location's visible units, grouped when identical. Use a canonical location KID from GetBankLocations and reuse your manager bearer session. Requires an active manager, an assigned Tab, Location Read, Unit Read, matching site/bank/location scope and RetentionDays visibility. Opening hours never grant access.
const response = await fetch(apiBase + '/api/v1/locations/' + encodeURIComponent(locationKid) + '/opening-hours', {
headers: { Authorization: 'Bearer ' + token, 'Accept-Language': 'en-GB' }
});
if (!response.ok) throw new Error('Opening hours unavailable: ' + response.status);
const hours = await response.json();
for (const group of hours.groups) {
console.log(group.units, group.weekly, group.exceptions, group.isOpenNow, group.nextChange);
}
The response contains locationKid, timeZone, calculatedAt and groups. Each group has units:[{kid,name}], weekly, exceptions, nullable isOpenNow and nullable nextChange. Schedule rows contain label,status,opens,closes,closesNextDay,daysOfWeek,date. Stable status strings are Open, Closed, AllDay and Unknown. Only Open has HH:mm times; closesNextDay identifies midnight/overnight closing. Weekly rows use ISO weekdays 1–7 (Monday–Sunday) and null date; exception rows use a YYYY-MM-DD date and an empty weekday array. Labels/names use Accept-Language. Empty groups is valid.
Opening and closing fields independently inherit previous weekdays; an unset unit week inherits its registered controller. Explicit unit exceptions still apply. A known controller with no weekly limits is AllDay. Missing, hidden or cyclic owners produce Unknown; never interpret null isOpenNow as closed or open. Equal explicit times mean Closed; short intervals are preserved. Priority is Custom1, Custom2, Custom3, configured holidays, first Wednesday, weekly schedule. One exception is returned per date, from today through two calendar months inclusive, across year boundaries. Leap-day exceptions apply only in leap years. Configured May 1, Constitution Day and legacy Great Prayer Day rules are supported. An explicit date exception replaces the previous day's overnight spill.
Times follow the location's TimeZoneId/legacy TimeZone, then the bank's, defaulting to Europe/Copenhagen when absent. DST gaps advance to the first valid minute; repeated times use the first opening and last closing occurrence. nextChange includes its UTC offset and is null when unknown or no transition is found within 369 days. calculatedAt is the observation time; reload when current status is needed. These are planned hours, not equipment readiness or guaranteed access. Values are read on request; avoid polling more often than once per minute and pause hidden pages.
400: invalid/wrong-site KID; 401: sign in again; 403: missing scope/Tab/read rights (reason may be missing-location-read or missing-unit-read); 404: absent or retention-hidden location; 503: unavailable, invalid or oversized data. Show 503 as unavailable, with manual retry. Up to 255 visible units; no writes. Included in client packages 0.3.2 for this beta release.
Location reservation rules
GET /api/v1/locations/{locationKid}/booking-rules, operationId GetLocationBookingRules, reads configured rules for visible units. Reuse the manager bearer session and a canonical location KID from GetBankLocations. Requires an active manager, an assigned Tab, Location Read, Unit Read, matching site/bank/location scope and RetentionDays visibility. Rules do not grant access.
const response = await fetch(apiBase + '/api/v1/locations/' + encodeURIComponent(locationKid) + '/booking-rules', {
headers: { Authorization: 'Bearer ' + token, 'Accept-Language': 'en-GB' }
});
if (!response.ok) throw new Error('Reservation rules unavailable: ' + response.status);
const policy = await response.json();
for (const group of policy.groups) {
console.log(group.name, group.units);
for (const rule of group.rules) console.log(rule.code, rule.text, rule.warning);
}
The groups array is the compact layout calculated by the API. Each section contains name, units, rules, common and optional localized help. Identical rules are combined. A common section contains shared rules, followed by sections with differences. Render sections in order, using unit names when name is null. Quotas remain separate for each reservation group. There is no separate displayGroups field. All text is plain text.
Response: locationKid, calculatedAt (UTC), groups:[{name,units,rules}]. Each group has nullable configured name, units:[{kid,name}] and rules:[{code,text,warning}]. Unit KIDs are canonical and authorized. Names/text follow Accept-Language; render as plain text, never HTML. Stable codes: Method, ReservationLimit, BookingHorizon, ReservationPrice, NoShowRelease, NoShowFee, BeforeStart, EarlyRelease, CurrentTurn, OutsideTurns, Dependency, DryingRoom, SettingsConflict, InvalidCalendar, UnknownSettings. Display the text and warning flag for future codes too.
For optional value emphasis, each rule also provides parts:[{text,isValue}]. Concatenate the ordered text parts without adding separators to reproduce rule.text. Values such as counts, amounts and minutes have isValue:true; other text has false. Placement follows the selected language, including repeated values. The API sends no HTML or Markdown formatting. All parts are plain text, including stored names that happen to contain markup-like characters. Existing text, code and warning fields remain unchanged; older clients may ignore parts. If parts is absent or empty, render text. Example in English:
{
"code": "NoShowRelease",
"text": "If the resident does not arrive, the reservation is released 30 minutes after the slot starts.",
"warning": false,
"parts": [
{ "text": "If the resident does not arrive, the reservation is released ", "isValue": false },
{ "text": "30", "isValue": true },
{ "text": " minutes after the slot starts.", "isValue": false }
]
}
Web clients can create their own emphasis elements using textContent. Never use innerHTML for either representation:
for (const part of rule.parts?.length ? rule.parts : [{ text: rule.text, isValue: false }]) {
const node = document.createElement(part.isValue ? 'strong' : 'span');
node.textContent = part.text;
row.append(node);
}
For optional value emphasis, each rule also provides parts:[{text,isValue}]. Concatenate the ordered text parts without adding separators to reproduce rule.text. Values such as counts, amounts and minutes have isValue:true; other text has false. Placement follows the selected language, including repeated values. The API sends no HTML or Markdown formatting. All parts are plain text, including stored names that happen to contain markup-like characters. Existing text, code and warning fields remain unchanged; older clients may ignore parts. If parts is absent or empty, render text. Example in English:
{
"code": "NoShowRelease",
"text": "If the resident does not arrive, the reservation is released 30 minutes after the slot starts.",
"warning": false,
"parts": [
{ "text": "If the resident does not arrive, the reservation is released ", "isValue": false },
{ "text": "30", "isValue": true },
{ "text": " minutes after the slot starts.", "isValue": false }
]
}
Web clients can create their own emphasis elements using textContent. Never use innerHTML for either representation:
for (const part of rule.parts?.length ? rule.parts : [{ text: rule.text, isValue: false }]) {
const node = document.createElement(part.isValue ? 'strong' : 'span');
node.textContent = part.text;
row.append(node);
}
Only identical calendars and complete rule settings are grouped. SettingsConflict means one calendar contains different policies; separate display groups do not create independent quotas. Counts describe upcoming reservations per resident, within the calendar group or globally. Positive week limits include the current calendar week; zero uses the legacy 400-day horizon. Missing before/after/add-time settings default to 15 minutes; drying-room search defaults to 4320 minutes. Malformed values produce warnings instead of permissive claims. V3 calendars are supported; absent calendars are omitted except instant reservations. Empty groups is valid.
Currency comes from registered unit, visible controller, location or bank settings, never the UI language. Unknown currency is explicitly labelled with warning=true. Hidden dependency names are not returned. This configuration snapshot is not live equipment readiness, remaining resident quota or a booking validation result. No resident bookings are loaded and nothing is written. Refresh on navigation; do not poll more than once per minute.
400: invalid/wrong-site KID; 401: sign in again; 403: missing scope/Tab/read permission; 404: absent or retention-hidden location; 503: busy, unavailable, invalid or oversized storage. Show 503 as unavailable with manual retry, never unrestricted booking. Bounded to 255 visible units, 8192 Log24 values, an eight-second settings deadline and two concurrent settings reads. Included in client packages 0.3.2 for this beta release.
Unit progress
Each unit returned by GetLocationUnits, GetUnitOverview and GetUnitGroup includes progress: {status, percent, remainingSeconds, calculatedAtUtc}. The API calculates this read-only estimate from the same bounded Log24 snapshot; no additional permission or endpoint is needed. Existing manager, Tab, Location Read, Unit Read, resource scope and retention checks still apply. Poll at most every ten seconds and pause hidden pages.
const response = await fetch(apiBase + '/api/v1/units/' + encodeURIComponent(unitKid), {
headers: { Authorization: 'Bearer ' + token }
});
if (!response.ok) throw new Error('Unit request failed: ' + response.status);
const { unit } = await response.json();
const progress = unit.progress;
const percent = progress?.percent; // null means unknown/inapplicable, never zero
Estimated supplies 0–99% and estimated remaining seconds from positive Started/Done MS2000 values. Complete supplies 100% only for the DONE cycle range, excluding the LinkOnline connection marker. A passed estimate returns EstimateExpired; it does not prove physical completion. Reset/invalid/missing times, the unknown-end marker, conflicting sequence DocIds or fractional Connected quality return UnknownEndTime during an active cycle. Both have null percent and remainingSeconds; show an indeterminate bar.
Disabled, OutOfOrder, AutoOutOfOrder, Repair, Disconnected, Error, Idle and Unknown have no percentage. Only Enabled=1 enables a unit. Connected=0 or a disconnected cycle suppresses progress; absent Connected is not assumed offline and no threshold is invented for fractional quality. Times come from state Text; TagId is a sequence ID. Clients must not calculate percentages from cycle enum ordinals. calculatedAtUtc is calculation time, not proof of fresh hardware contact. Inputs can be ten seconds old or reflect an older device report. Missing/new status codes should render as unknown. Handle 401 by signing in, 403/404 as unavailable, and 503 with a later retry; do not keep presenting an old estimate as current.
Lazy unit icons and offline status
GET /api/v1/units/icons?kid={unitKid}&kid={anotherUnitKid}, operationId GetUnitIcons, accepts 1–32 canonical unit KIDs from this site. Fetch only visible icons, deduplicate and batch requests. Requires an active manager, an assigned Tab, Location Read, Unit Read, matching bank/location scope and retention-visible units and locations. Every lookup reapplies these checks before Alive is read.
const query = new URLSearchParams();
visibleUnitKids.forEach(kid => query.append('kid', kid));
const response = await fetch(apiBase + '/api/v1/units/icons?' + query, {
headers: { Authorization: 'Bearer ' + token }
});
if (response.status === 401) throw new Error('Sign in again');
if (!response.ok) throw new Error('Unit icon lookup failed');
const { items } = await response.json();
// For status === 200, use iconKid unchanged in /api/v1/icon/{iconSet}/{kid}.svg.
Each item contains kid, iconKid, offline and status. Exactly Alive.Offline = 1 adds eIcon.error as Kid.Icons[1]; the API retains the primary icon, unit number and other presentation fields. Alive.Offline = 0 sets Kid.Icons[1] to eIcon.check, replacing any offline decoration. A missing Alive row, null or other Offline value yields offline: null and no status decoration. No MainId inference or Cycle heuristic is used.
400 rejects malformed, foreign or oversized input; 401 requires login. Per-item 403 means missing access, 404 means absent/retention-hidden, and 503 means unavailable; these items have null iconKid/offline and must not be interpreted as online. On transient failure retain the last confirmed image and retry later. Reads are bounded to the exact tenant/bank/location and cached for up to 10 seconds; HTTP responses are no-store. Refresh at most once per 10 seconds while visible, pause hidden pages and prevent overlapping requests. The public image endpoint remains anonymous and performs no Alive lookup. This adds no writes or hardware commands.
Bank icons and combined unit status
GET /api/v1/banks/icons?kid={bankKid} · operationId GetBankIcons. Accepts 1–8 canonical bank KIDs from this site; repeat kid. Requires an active manager, assigned Tab, Bank Read, Location Read, Unit Read and matching scope. Location-only grants include only those locations. Disabled locations require all-bank access; location and unit RetentionDays visibility both apply.
Any included unit with Alive.Offline=1 gives eIcon.error as the second Kid.Icons item. A nonempty set entirely known Offline=0 gives eIcon.check. Empty/incomplete data gives offline=null without added status, unless a unit is confirmed offline. Orphan Alive rows are excluded. Use opaque iconKid unchanged in the icon URL; GetIconPresentation adds count/colour while preserving status.
400: invalid/foreign/oversized input. 401: sign in again. 503: session storage unavailable. Per-item 403 means missing access; 503 means unavailable/ambiguous/oversized data, with null iconKid/offline. Failure never means online. Fetch visible banks only, deduplicate, refresh at most every ten seconds and pause hidden pages. Status cache: ten seconds per authorized location set; labels/locations: sixty seconds. HTTP no-store. Read-only Log24/Alive; up to 65,536 units per bank and 128 locations per SQL batch. Authorization is reapplied on every call.
Location icons and combined unit status
GET /api/v1/locations/icons?kid={locationKid}&kid={anotherLocationKid}, operationId GetLocationIcons, accepts 1–32 canonical location KIDs from this site. Requires an active manager, an assigned Tab, Location Read, Unit Read, matching bank/location scope and RetentionDays visibility, exactly as GetLocationUnits. Authorization precedes every status lookup.
const query = new URLSearchParams();
[...new Set(visibleLocationKids)].slice(0, 32).forEach(kid => query.append('kid', kid));
const response = await fetch(apiBase + '/api/v1/locations/icons?' + query, {
headers: { Authorization: 'Bearer ' + token }
});
if (response.status === 401) throw new Error('Sign in again');
if (!response.ok) throw new Error('Location icon lookup failed');
const { items } = await response.json();
// For status === 200, use iconKid unchanged in /api/v1/icon/{iconSet}/{kid}.svg.
Each item has kid, iconKid, nullable offline and status. Any unit in the authorized overview with Alive.Offline=1 gives offline:true and eIcon.error in Kid.Icons[1]. Only a nonempty set where every unit has Alive.Offline=0 gives offline:false and eIcon.check. Otherwise status is null, with no added status icon. A known offline unit takes precedence over missing data. Empty locations, missing/invalid Alive values, hidden units and orphan Alive rows never establish online status. Only units returned by GetLocationUnits contribute. The primary location icon and LocationId text are preserved.
400 rejects malformed/foreign KIDs or invalid batch size; 401 requires login. Per-item 403 means denied access, 404 means absent or retention-hidden, and 503 means temporarily unavailable, with null iconKid/offline. A failure never means online. Retain the last confirmed image on transient failures. Read visible icons only, batch duplicates, pause hidden pages and refresh at most every 10 seconds without overlapping requests. Location and unit lookups share the bounded Alive cache for up to 10 seconds. Responses are no-store; unchanged image URLs can be reused without resetting the image. No database writes or hardware commands; public image rendering does not query Alive.
Unit type and groups
GetLocationUnits now includes unitType (number), unitTypeName (eUnitType name when defined) and unitTypeSource. Log24 supplies the type in the same query as the other columns.
GET /api/v1/units/{unitKid}, operationId GetUnitOverview, returns {location,unit,descriptorAvailable,settingGroups,stateGroups}. Use a canonical unit KID from the location list. Requires an active manager, at least one Tab, explicit Location Read and Unit Read, and matching site/bank/location access. Both unit and location must be visible within RetentionDays.
A present UnitType2 is the direct type and takes precedence, even if malformed. Otherwise UnitType is decoded with FlexOrm's (value >> 1) & 63 rule. Missing/invalid types are null, never an invented Type000; unknown numeric types are retained.
Groups come from the installed Kombine.Flex.Units descriptor packages: distinct eSettingGroup/eStateGroup names ordered by numeric value. Missing or unsupported types give descriptorAvailable=false and empty groups. These are metadata only, without setting/state values, editing rights or hardware access.
Unit data is cached for up to 10 seconds; location labels and manager snapshots for up to 60 seconds. Authorization is checked on every request. Accept-Language localizes names/cycle labels, not group identifiers. Poll at most every 10 seconds and pause hidden pages.
400: invalid/foreign KID; 401: sign in again; 403: missing Tab/scope/read access; 404: missing or retention-hidden unit/location; 503: retry later. After scope checks, 403 may have reason missing-location-read or missing-unit-read.
The portal uses this operation at /units/{unitKid}. Clicking a unit row adds its shortcut under the location in the workspace and shows its type's groups. Removing the shortcut never deletes the unit.
Read a unit group's settings or states
GET /api/v1/units/{unitKid}/groups/{kind}/{group}, operationId GetUnitGroup. Set kind to settings or states, and use an exact group identifier from GetUnitOverview. Returns {location,unit,kind,group,items:[{name,valueType,scope,valueStatus,value,ms2000}]}. The API determines the fields from the unit type. The same active-manager, Tab, Location Read, Unit Read, site/resource and RetentionDays checks apply before reading any values.
const response = await fetch(`${api}/api/v1/units/${encodeURIComponent(unitKid)}/groups/states/Widget`, {
headers: { Authorization: `Bearer ${token}`, "Accept-Language": "en-GB" }
});
if (!response.ok) throw new Error(`Group request failed: ${response.status}`);
const page = await response.json();
for (const field of page.items) console.log(field.name, field.valueStatus, field.value);
One bounded Log24 read returns the newest MS2000 per declared current-unit field. Missing rows have valueStatus=missing; stored null/empty strings remain stored. No descriptor default or older value is substituted. MainUnit and other-object bindings have status other-scope and no value; their owning KID is not inferred. Hidden settings are omitted and credential settings are redacted without reading their values. Fields and groups retain stable enum names. This operation is read-only; no hardware commands or edits.
Values are uncached; type/location/manager snapshots retain the existing 10/60-second limits. At most 512 fields and 16,384 characters per value; ambiguous/oversized data fails the whole request with 503. Poll settings no faster than every 5 seconds and states every 10 seconds and pause hidden pages. 400: invalid kind, syntax or KID/site; 401: sign in again; 403: missing permissions; 404: invisible unit/location or group not declared for the current type; 503: try later. After scope checks, 403 may include missing-location-read or missing-unit-read. The portal lazily opens the group and stores its shortcut below the unit in the workspace; removing it never changes device data.
Live sync and setting history
hasHistory is true when the setting has at least one bank Log2 history record. Show the history control only when both canReadHistory and hasHistory are true; the flag does not grant permissions. History is still fetched on demand.
GetUnitGroup adds sync, changedBy:{kid,kind,name,iconKid} and canReadHistory for settings. Sync comes from the exact bank Log2 record matching the displayed value: only 1 is acknowledged; other values are pending, and null means unknown. A sync-only change does not change MS2000 or invalidate the value revision. Poll settings at most every five seconds (states every ten), with no overlap and a pause on hidden pages.
GetUnitSettingHistory uses the same active manager, assigned Tab, location scope, Location Read, Unit Read and RetentionDays checks. Audit labels grant no access to the editor's account or directory. Only declared current-unit settings are eligible; hidden, credential and other-scope fields are excluded.
The response is {unitKid,group,setting,items:[{value,ms2000,sync,changedBy}],nextBeforeMs2000}, newest first. Request the next page with &beforeMs2000=NEXT_CURSOR; null means the end. Limit is 1–50, default 25. Open history on demand; do not poll one history request per row. Names/icons are current Log7 labels, not historical snapshots. Manager/service labels use bank zero, installers the tenant bank and residents the unit's bank; unknown identities can have no KID/name. changedBy is null when no editor is recorded (UserId zero); display no user icon or name in that case. Unknown nonzero identities remain present.
400: invalid input/site; 401: sign in again; 403: missing read access; 404: unavailable unit/group/setting; 503: storage failure or ambiguous/oversized values. No automatic retry loop. Each call has an eight-second storage deadline and a 16,384-character value limit. No database writes, count query or history cache.
Edit a unit setting
SetUnitSetting: POST /api/v1/units/{unitKid}/groups/settings/{group}/{setting}. GetUnitGroup now also returns canEdit, revision, required, minimum, maximum and selectable options. Editing requires Unit Write as well as the read operation's account, Tab, scope and retention permissions. The API rechecks the current manager and unit type inside the write transaction.
Send invariant text in value, up to 4096 characters, and the field's expectedRevision. Type, required, range, pattern and selectable-option rules come from the descriptor. Booleans normalize to 0/1. No defaults are inserted. States are always read-only. Hidden, device-owned, read-only, ORM-managed and credential settings are not editable. MainUnit/other-object bindings and dynamic option sources remain unsupported for editing.
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
const path = `${api}/api/v1/units/${encodeURIComponent(unitKid)}/groups/settings/Core`;
const read = await fetch(path, { headers });
if (!read.ok) throw new Error(`HTTP ${read.status}`);
const field = (await read.json()).items.find(item => item.name === "Name");
if (!field?.canEdit) throw new Error("This setting cannot be edited");
const saved = await fetch(`${path}/Name`, {
method: "POST", headers,
body: JSON.stringify({ value: "Washer 1", expectedRevision: field.revision })
});
if (!saved.ok) throw new Error(`HTTP ${saved.status}; reload before retrying`);
const confirmed = await saved.json(); // value, ms2000, revision
A successful response confirms storage, not delivery to the device: it returns the canonical unit KID, group, setting, value, MS2000 and new revision. The change is appended to bank Log2 with Sync=0 and the editor's UserId; the current Log24 projection is verified before commit. 400: invalid value; 401: sign in again; 403: write denied; 404: unit/group/field unavailable; 409: value or type changed; 503: storage failure. After a conflict or lost response, read back before editing again. Never automatically replay a write.
In the portal, clicking one setting group adds all that unit's setting groups to the workspace. Clicking a state group adds all state groups. Settings save on blur and selections save immediately; the confirmed value is shown only after API success. Polling pauses during editing, saving or an unresolved error.
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.
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.
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.
Map1: Public purchase map
GET /api/v1/public/displays/Map1 · 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.
Use /api/v1/public/displays/Map1. The old disp73 path has been removed and returns 404. The operation ID remains GetPublicDisp73 (JavaScript: getMap1()).
const response = await fetch('https://api.team.kombine.technology/api/v1/public/displays/Map1');
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.
Users2 uses Log tables exclusively for business data. Resident IDs, number uniqueness, tag ownership and changes use Log7; synchronization is requested through Log2. Obsolete Users and Settings tables are neither read nor written. API routes and commands are unchanged.
Users2: resident editing and export
GetBankUsers supports locationKid and deleted=all|active|deleted|no-access. Locations must belong to the same bank and the manager's grants. no-access selects visible residents without recorded Access in any location the manager may see. Empty location lists and NoAccess-only lists match. Bank-wide access evaluates the entire bank; hidden locations do not affect the result for location-restricted managers. Residents without an association to a permitted location remain hidden from those managers. Retention still applies, so visible deleted residents may match. A selected locationKid additionally requires an association to that location. Filtering precedes paging and also applies to CSV export. Keep these filters with cursors; restart without a cursor when changing them. Unknown filter values return 400, missing permissions 403, and unavailable data 503.
const page = await client.getBankUsers(bankKid, {
sort: 'number', deleted: 'no-access', pageSize: 25
});
// For the next page, retain these options and add cursor: page.nextCursor.
GET /api/v1/banks/{bankKid}/users/{userKid}/workspace (GetBankUserWorkspace) returns authoritative edit fields and an opaque revision. Requires Users2, User Read and bank-wide/tenant-wide access. Location-only managers can still read their list and export, but cannot edit shared residents through these operations.
POST /api/v1/banks/{bankKid}/users (CreateBankUser) accepts {"action":"create","name":"New resident","number":"001"} and returns HTTP 201 with the resident KID. Requires User Create. Assign tags and location access separately.
POST /api/v1/banks/{bankKid}/users/{userKid}/commands (ExecuteBankUserCommand) requires the current revision. All commands require User Read. Attributes require Write; tag/location require Create; delete/restore require Delete; replace requires both Create and Delete. Profile requires Rename for a changed name and RenameExtrenatId for a changed number. All require Users2 and bank-wide scope. Manager identity and tenant come only from the session and trusted site configuration.
// baseUrl and token from the login example; bankKid/userKid are KIDs returned by the API.
const path = baseUrl + '/api/v1/banks/' + encodeURIComponent(bankKid) + '/users/' + encodeURIComponent(userKid);
const headers = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
const read = await fetch(path + '/workspace', { headers });
if (!read.ok) throw new Error('HTTP ' + read.status);
const current = await read.json();
const saved = await fetch(path + '/commands', { method: 'POST', headers,
body: JSON.stringify({ action: 'profile', revision: current.revision, name: 'Updated name', number: current.number }) });
if (saved.status === 409) throw new Error('Reload and review current details before retrying');
if (!saved.ok) throw new Error('HTTP ' + saved.status);
const updated = await saved.json();
Resident icons: GetBankUserWorkspace and command responses include iconKid, availableIcons and canEditIcon. To change an icon, call ExecuteBankUserCommand with action: "icon", the current revision and an exact eIcon name with eIconSubject.Person metadata. This requires Users2, User Read, User Write and a bank-wide grant. A current non-Person icon appears first for display only; do not submit it as a new choice. Icons are served locally through /api/v1/icon/g/{kid}.svg.
{"action":"icon","revision":"<revision from GetBankUserWorkspace>","icon":"user"}
Icon changes do not require or change the resident's name or number. Only display the returned icon and use the new revision after HTTP 200; serialize edits that share the revision. Invalid icons return 400, missing permission 403, stale revisions or deleted residents 409, and storage failures 503. After 409 or an uncertain network outcome, reload the workspace and review it before retrying. No-op clicks on the selected icon should not send a write.
profile: name and number. Checks number format and existing active numbers.
attributes: the complete attributes list with canonical eUserAttribute names and value (-1 means no numeric value).
tag: tagKid and state (Unlocked, Locked or Deleted). The canonical KID must belong to this bank. A tag owned by another resident is rejected; historical tags cannot be modified.
location: locationKid and state (Access or NoAccess). The location must be active.
delete: optional deleteAtUtc. Empty/past means now and deactivates tags. Future deletion can be scheduled up to 366 days ahead.
restore: restore a resident or cancel scheduled deletion. Deleted tags are not automatically reactivated. Retention limits restoration.
replace: name, number and optional deleteAtUtc. Deleting the old resident and creating the new one share one transaction. Balance, tags and location access are not transferred. Future deletion does not release the old number yet.
HTTP 400: invalid fields/KID/number format. 401: invalid session. 403: insufficient rights. 404: unavailable within retention. 409: revision conflict, duplicate number or tag in use. 503: write connection/storage unavailable. ProblemDetails includes a stable code when possible, such as conflict, number-exists, tag-in-use or writes-unavailable. After a network failure on POST, read back and check the result before retrying; do not assume rollback.
GET /api/v1/banks/{bankKid}/users/export (ExportBankUsers) returns UTF-8 CSV with semicolons and stable column identifiers. Same filter/sort/direction/locationKid/deleted and rights as the list. Maximum 10,000 residents and 4 MiB text; HTTP 422 requires narrower filters and never returns a partial file. Each page rechecks authorization; this is not a transactionally frozen snapshot. CSV does not contain activation codes or balances.
GET /api/v1/banks/{bankKid}/users/{userKid}/activation (GetBankUserActivation) returns activation-letter data including activationCode. Requires bank-wide User Create; deleted residents are rejected. Treat the code as a credential: do not log or cache it.
Changes record the manager ID and request synchronization through the existing backend. Success confirms storage, not delivery to physical equipment. Synchronization and scheduled deletion require the existing backend. EVaskeri/CP autologin, subscription management and test/information messages have not been migrated. Local convenience login grants no additional permissions.
Reservations · Bookings1
GetBankBookings: GET /api/v1/banks/{bankKid}/bookings. Requires Bookings1 (8), Location/Unit/User Read and bank-wide or specific location grants. Location grants constrain both rows and filter options. All object identifiers are canonical KIDs belonging to the site's tenant.
Filters: from/through are inclusive local start dates; defaults are UTC today's date minus 7 through plus 90 days, at most 367 dates. Optional locationKid, unitKid, userKid, search (resident name/number, at most 100 characters) and status=all|active|cancelled. limit is 1–200, default 50. offset is 0–100000; advance by limit while hasMore is true. Pages are not a locked snapshot; refresh after changes.
items contains the latest current event per location/unit/resident/start. Superseded Log5 rows (nonzero Period) and AUT/isy codes are excluded. startLocal/endLocal are local times without a UTC offset: never append Z. recordedAtUtc is the UTC event time. Weekly reservations instead have a weeklyMinute position 0–10079 and are always included regardless of date filters. durationMinutes is positive; cancelled expresses cancellation. source retains the system code, including ARR/ArR (attendance), FE0 (no-show) and FE1 (no-show with fee).
ExecuteBankBookingCommand: POST /api/v1/banks/{bankKid}/bookings/{bookingKid}/commands with {"action":"cancel"} or {"action":"restore"}. Use the reviewed row's KID, which also identifies its revision. Commands additionally require Unit Write and a grant for the reservation's location. canCancel/canRestore are presentation hints; the API always rechecks authorization and the current version.
const booking = page.items.find(item => item.canCancel);
if (!booking) throw new Error('No reservation on this page permits cancellation.');
const response = await fetch(`${base}/api/v1/banks/${encodeURIComponent(bankKid)}/bookings/${encodeURIComponent(booking.kid)}/commands`, {
method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'cancel' })
});
if (!response.ok) throw await response.json();
const changed = await response.json();
A command appends a Log5 event with the duration's sign reversed, Currency SER and Sync 0, retaining history. HTTP 200 confirms storage; synced=false means existing backend synchronization is pending. No direct device command is sent. 400: invalid filter/KID/action; 401: invalid session; 403: insufficient permission; 409 conflict: stale event, or time-occupied: overlapping booking; 422 weekly-restore-unavailable: unsupported weekly restoration; 503: unavailable storage/write connection. After a network failure, read the list again before considering another attempt. Never automatically retry POST.
Limitations: no creation, recurrence expansion, export or fee changes. Raw weekly positions are displayed without guessing the first weekday; restoring weekly reservations is not supported yet. An active weekly reservation conservatively blocks restoration on that unit. Restoration checks overlaps but does not recalculate controller booking rules. Writes require InnoDB and at most 20,000 current events on the unit; filters are bounded to 4,096 names. No caching or automatic polling is used.
Transactions · Account2
Receipt grouping and FlexOrm decoding
Transaction descriptions now use the shared legacy decoder used by FlexOrm, including program, duration, soap and transfer details. Each item additionally has documentKey (an opaque bank-scoped string), documentId (positive DocId or null), isAnonymized (boolean) and paymentKind (Credit, ReserveRefund, Managed, or an empty string). Never parse the display description to identify a payment operation; payment IDs are not exposed.
documents is an additional array of {key, docId, lines, totals} covering this page only. Lines group by DocId within the same original resident, location and period, across units. Invalid/missing DocId and payment-managed lines stay separate. Documents sort by their earliest loaded line, newest document first; lines sort chronologically. Document totals cover its loaded lines, separately by currency. A filter or page boundary may cut a receipt: these are not complete invoice totals. Flat items, offset/limit, hasMore and full-selection totals keep their existing meaning. Merge further pages by documentKey, deduplicate by posting kid and require equal revisions; never fetch outside the authorized filters to complete a document.
for (const document of page.documents) {
console.log(document.key, document.docId, document.totals);
for (const line of document.lines) console.log(line.kid, line.description);
}
Retention and payment-managed entries
Tenant Log24 LawAccountingYears applies to all entries; LawSurveillanceDays also applies to zero-amount entries. A positive manager Log7 value overrides the corresponding tenant setting; a year is 365 days. Missing, invalid or zero values mean no identity retention. Entries older than either applicable cutoff are returned in bank/location/unit views with the bank's GDPR user KID (UserId 1000), empty userName/userNumber, a safe transaction-type description, isAnonymized=true and canReverse=false. Their amounts remain in totals. With an explicit userKid, expired entries are excluded before pagination and totals. The same rules apply to CSV/XLSX; their existing column layout is preserved.
Revision checks include retention visibility and are cached separately for each authenticated manager; the same manager's identical authorized selection still shares a maximum 30-second cache. There is no client-supplied manager override. Managed payment entries expose only paymentKind; ordinary reversal is unavailable even if the stored transaction type resembles consumption. Reversal also rechecks retention in its write transaction. These ineligible requests return 422 reversal-unavailable. Existing 400/401/403/409/503 handling remains applicable. Do not automatically retry an uncertain financial write.
This ports the listing behavior without a FlexOrm runtime dependency, legacy business tables, external payment refund calls or payment-metadata lookups outside Log. Generated clients and downloadable packages are synchronized in version 0.3.2.
GetBankAccount: GET /api/v1/banks/{bankKid}/account. Requires Account2 (2), Bank/Location/Unit/User Read and a bank or location grant. The current manager account, credential stamp and site's tenant are checked before any business read. Location grants constrain rows, totals and filter options. Reuse the ordinary manager bearer session.
Dates are inclusive in timeZone, defaulting to today in Europe/Copenhagen, with daylight-saving transitions respected. At most 367 dates. A nonnegative period replaces the date interval; zero means the current period. The latest 100 available period IDs are returned. Optional locationKid, unitKid and userKid must be canonical KIDs in this bank. kind is all, debit (negative) or credit (positive). includeZero, includeBookings, includeMonthly default to true. Limit is 1–200 (default 50); offset is 0–100000. Rows are newest first, then location/unit. Separate requests may shift as new postings arrive.
items contains the immutable posting KID, scope KIDs, recordedAtUtc, signed amountMinor, currency, description, transaction type, period, current display labels, reversed, optional reversalOfKid and canReverse. totals covers the full filtered selection, separately by currency. These totals are movements, not resident balances; no currency conversion is performed. Portal amounts display legacy hundredths as amountMinor / 100. Labels are current Log7/Log24 values, not historical snapshots. Modern JSON, plain legacy text and JSON wrapped in a legacy prefix/start-code envelope are supported.
recordedAtUtc / MS2000 is event time, not insertion time. Postings can arrive minutes or days later. Never use the highest MS2000 as a watermark for all new database entries. Each page includes a revision for the entire filtered set: identities, periods and monetary totals by currency. It is independent of offset/limit and is neither a cursor nor an access grant.
GetBankAccountRevision: GET /account/revision with the same filters and permissions. Returns only {"revision":"..."}. Revision reads for identical authorized selections are shared for at most 30 seconds per API instance; every request rechecks access. Poll no faster than every 30 seconds and pause hidden views. On change, reload the loaded range from offset 0. Combine only pages with equal revisions and replace the old list only when all required pages are ready. If the revision changes during loading, discard the incomplete result and retry later with backoff.
const check = await fetch(`${accountUrl}/revision?${filters}`, {
headers: {Authorization: `Bearer ${accessToken}`}, cache: 'no-store'
});
if (check.status === 401 || check.status === 403) {
clearAccountView(); // remove data and handle login/access
} else if (!check.ok) {
showRetryLater(); // on 503, wait and increase backoff for repeated errors
} else if ((await check.json()).revision !== page.revision) {
await reloadLoadedAccountPages(); // offset 0; require equal revisions on all pages
}
Updates are not instantaneous: polling plus the shared cache can delay detection by approximately one minute. Revision checks inspect the filtered Log1 set and assume no maximum arrival delay. Names and reversal flags can change independently of the revision; reload as needed and always recheck commands on the server. Under continual changes or timeouts the old view is retained with retry available. CSV/XLSX remains a single database-snapshot export.
ExportBankAccount: GET /account/export with the same filters and format=csv or format=xlsx. Pagination is ignored. It returns the complete selection in one snapshot, at most 10000 rows and 4M description/name characters. Oversized exports fail, never silently truncate. Money columns use signed minor units, dates are UTC, identifiers/headers are stable. CSV neutralizes spreadsheet formulas; XLSX uses text cells for untrusted values.
ReverseBankAccountEntry: POST /account/{transactionKid}/reversal, without a request body. Additionally requires a bank-wide grant and Bank/User Write. Only recognized negative resident consumption with internal physical/activation tags can be reversed. The server locks and rechecks the original, appends the opposite signed amount in the current period, retains document/program metadata and requests balance/access synchronization. The original entry is preserved. Payment-provider transfers, anonymous/system users, credits, monthly transfers, unknown transaction types and already reversed entries are unsupported. canReverse is only a display hint; the command always validates again.
// Only after your own UI/user has reviewed and confirmed this exact entry.
const reversed = await fetch(`${accountUrl}/${encodeURIComponent(entry.kid)}/reversal`, {
method: 'POST', headers: {Authorization: `Bearer ${accessToken}`}
});
if (!reversed.ok) {
const problem = await reversed.json();
throw new Error(problem.code || `HTTP ${reversed.status}`);
}
Errors: 400 invalid filter/KID/time zone; 401 expired, stale or inactive session; 403 insufficient Tab/resource/operation grants; 404 missing posting; 409 already-reversed; 413 data-limit; 422 reversal-unavailable; 503 storage/write connection unavailable or nontransactional storage. Framework validation errors use the standard ProblemDetails errors object. Never automatically retry a financial write, including after timeout: refresh the list and review its result first.
Limits: read/write only Log tables; no schema changes, provider refunds, bank-to-bank transfers, balance reset or direct hardware delivery. InnoDB is required for financial writes; queued synchronization is not confirmation of delivery. At most 4096 filter labels and 100 currency groups. No caching or polling. The portal's print/PDF action prints only the loaded page; use export for the full selection.
Incremental bank search
eTab.Banks2 (5) is returned in profile tabs and tabDetails when granted to the manager in Log7. Global Search requires Bank Read, at least one current Tab and the manager's resource grants.
SearchBankActivation has a separate limit: 5 calls per 10 minutes and 20 per hour per manager in the tenant-bound API process, using fixed windows. All attempts count, including malformed and valid codes. Changing tokens or source IP does not reset the quota. HTTP 429 supplies Retry-After in seconds; wait at least that long before the next code attempt. Name searches remain available. The portal only sends code-shaped numeric queries to this provider and stops code requests during cooldown. Quotas are held in memory: restart resets them, and multiple API replicas require a shared limiter for an aggregate quota.
Call GET /api/v1/search/banks?q=Kombine (SearchBanks) and GET /api/v1/search/bank-activation?q=CODE (SearchBankActivation) concurrently with the same manager bearer session. Display each response immediately and merge items by kid. Each item contains kid, kind, name, zip and icon (the bank eSetting.Icon from the same Log24 query). Code matches have an empty icon; clients can use a default bank icon; kind is the enum name Bank. GetSearchBank: GET /api/v1/search/banks/{bankKid} resolves an authorized bank result before opening it.
Requires an active manager, at least one current Tab, PermissionBank2 Read and a matching site/bank grant. A location grant permits discovery of its parent bank only, without granting other locations. Every request checks the bounded session snapshot. The hostname determines the tenant.
Queries are trimmed and must contain 2–128 characters. Bank names and settlement emails use literal, case-insensitive substring matching. Only current bank-level Name, Icon and SettlementEmails in Log24 are read. The zip field remains empty for compatibility; banks have no postcode. Location postcodes are not searched. Values are, supporting JSON and legacy text. A text search returns at most 50 items; hasMore asks the caller to refine the query. A 60-second scope-specific cache coalesces simultaneous refreshes. The portal debounces input by 300 ms and ignores stale responses.
Bank codes are decoded with Kombine.Flex.Activation, then resolved in the same 60-second scoped Log24 cache as name searches. Results include the bank name and icon; zip remains empty. Invalid or unauthorized codes skip the bank lookup. Missing banks return no items. Bank, location and resident results are implemented. Errors: 400 query/KID, 401 session, 403 permission, 503 temporary storage failure. Retain other provider results on failure; avoid immediate retry loops.
SearchBanks also searches eSetting.SettlementEmails in Log24. Email search requires a whole-bank or tenant grant; location-only access permits bank-name discovery only. Results include matchedSetting (Name or SettlementEmails) and matchedValue containing the name or first matching email in the semicolon-separated list. Name matches take precedence. Render as text, never HTML. Example: GET /api/v1/search/banks?q=accounts%40example.test. The same cache, limits and error statuses apply; typing does not issue a new SQL query per keystroke.
Location search
GET /api/v1/search/locations?q=0123 (SearchLocations) runs independently of SearchBanks. It searches Log24 Name, Bank (alternative bank name), Zip, Address, VismaCustNo and TeltonikaSMS. Requires manager bearer, a current Tab, Location Read and a matching location grant. RetentionDays applies. Trimmed query 2–128 characters, literal case-insensitive substrings, at most 50 items plus hasMore. Results contain kind=Location, canonical kid, name, icon, zip and matchedSetting/matchedValue, with field priority in the order above. Render as text. Example: HTTPS GET with Authorization: Bearer and a URL-encoded q value.
GET /api/v1/search/location-activation?q=... (SearchLocationActivation) decodes via Kombine.Flex.Activation and resolves name/icon in the location cache. Location codes contain bank/location, not tenant; only the configured site is searched. Invalid, absent or inaccessible locations return no items. Bank and location codes share 5 calls per 10 minutes and 20 per hour per manager. Every code call counts; 429 includes Retry-After. The browser selects one code provider per input. 400: input; 401: reauthenticate; 403: permission; 503: temporary failure. Preserve other partial results on error. The 60-second cache coalesces misses; no query per location. Rate limits remain per-process and require shared storage for multiple replicas.
All bank and location searches, including activation lookups and GetSearchBank, include only BankId >= 1000. Special banks below 1000 are excluded.
Resident search
Resident results also include number from eSetting.Number. The portal displays Number · Name, omitting empty or duplicate parts. name retains its existing meaning; number is null for other result types.
Resident search and resident activation-code search also return parent banks with isContext=true when Bank Read is granted. At most 50 resident matches plus distinct banks; hasMore counts residents. Merge banks by kid with other results, including when direct bank search is disabled. Context banks do not include contact data.
A complete email address uses an exact case-insensitive match through the Log7 text index. Only names and TagIds support partial matching.
GET /api/v1/search/users?q=anna (SearchUsers) runs independently of banks/locations. Use Authorization: Bearer. Searches current Log7 Number, Name, Email, SMS and Tags. Number and email require exact case-insensitive matches; names allow literal substrings. TagId allows partial decimal IDs and spaces, but not hexadecimal. Name/TagId queries allow 10 seconds SQL and 12 seconds including queue time. Tags use the resident-list JSON/legacy parser.
Requires Users2, User Read and tenant/bank access or an Access/NoAccess association with a granted location. Ordinary users only, BankId >= 1000, RetentionDays applies. Maximum 50 visible results; up to 501 candidates examined. hasMore requests refinement when either bound is reached, so broad queries may omit matches. Returns kind=User, kid, name (number fallback), icon and matchedSetting/matchedValue, not other contact fields or tag lists. Priority: Number, Name, Email, SMS, Tags.
GET /api/v1/search/user-activation?q=... (SearchUserActivation) decodes via Kombine.Flex.Activation and resolves name/icon. Codes contain bank/user, not tenant; only the configured site is searched. All three code providers share 5 calls/10 minutes and 20/hour per manager. Invalid/invisible codes return no items. Errors: 400 input; 401 login; 403 access; 429 wait Retry-After; 503 temporary failure. Preserve other results. Cache 60 seconds per query/bank scope coalesces calls; no per-resident SQL. Quota remains per-process.
SearchLocations and SearchLocationActivation also return parent banks when the manager has Bank Read. Banks have isContext=true and contain name/icon, not contact data. At most 50 locations plus their distinct banks; hasMore counts locations. Merge by kid across responses, place each bank before its locations, and prefer direct bank matches over context explanations. Banks reuse the same 60-second cache as bank search. They are included even when only location search is enabled.
for (const item of response.items) {
if (!byKid.has(item.kid) || !item.isContext) byKid.set(item.kid, item);
}
SearchUserSms
GET /api/v1/search/user-sms?q=51573605 (SearchUserSms) is an independent fast provider with the same authorization, retention and parent-bank context as SearchUsers. It uses exact Log7.Text index lookups for plain and JSON values, normalizing spaces, hyphens, parentheses, leading + and international 00. Eight Danish digits match with and without 45. Accepts 8–15 digits; invalid format returns no items. Merge by kid. An independent 60-second cache and gate prevent slow substring/TagId queries from blocking SMS results. SearchUsers retains substring and TagId search and may still time out; keep SMS results on 503. Same 400/401/403/503 handling as SearchUsers; no activation quota is consumed.
Account endpoints retain HTTP 403 and code forbidden. After tab and bank-scope checks, the response may also include reason: missing-bank-read, missing-location-read, missing-unit-read or missing-user-read. Show a localized explanation; retain a generic denial for unknown reasons. No account data is read on denial.
Bookings: HTTP 403 retains code forbidden. After tab and bank-scope authorization, reason may be missing-location-read, missing-unit-read or missing-user-read. Localize the reason; use generic denial for unknown reasons. No booking data is read on denial.
GetCurrentManager returns Tabs and TabDetails ordered by eTab AttributeMetaSortOrder, then numeric tab ID. Missing metadata defaults to zero. Clients can display tabs in response order; permissions are unchanged.
SearchBanks, SearchLocations and SearchUsers accept complete canonical Kids and ToBankId (166.2000), ToLocationId (166.2000.4), ToUserId (166.2000-1100). Kid matches are exact, with matchedSetting=Kid and the readable identity in matchedValue. Existing site, tab, read, resource and retention checks still apply. Wrong tenant/type or inaccessible identities return no results. Resident lookup uses exact bank/user filters. Location/resident results retain authorized parent banks. No activation decoding or activation quota is used. HTTP errors and other text searches are unchanged.
GET /api/v1/search/users?q=166.2000-1100
SearchUsers supports kidOnly=true for direct resident Kid lookup even when ordinary resident search is disabled. Non-Kid input returns no items without business storage. Existing permissions apply; the toggle stays unchanged.
GET /api/v1/search/users?q=166.2000-1100&kidOnly=true
Kid search also accepts bank, location and resident identities without tenant: 2000, 2000.4 and 2000-1100. Kombine.Flex.Kid parses the identity; a missing tenant is filled exclusively from trusted API site configuration. An explicit tenant is preserved and checked. kidOnly=true also accepts tenant-relative resident IDs. Authorization is unchanged.
GetLocationUnits retains HTTP 403. After tab and resource checks, reason may be missing-location-read or missing-unit-read. Clients may show a localized explanation and use generic denial for unknown reasons. No location or unit data is read on denial.
{"status":403,"reason":"missing-unit-read"}
Account endpoints return HTTP 503 with code periods-timeout when the period query exceeds 10 seconds, storage-timeout for other SQL command timeouts, and storage-text-comparison for incompatible text collations. Show localized explanations and avoid repeated automatic retries. Raw SQL/database errors are not exposed. Date filters do not narrow the period lookup.
Account period choices use bank LogA (LocationId=0, UnitId=0, Period>0), up to 100 newest periods. Period zero remains selectable as the current period. Bank-wide access needs no Log1 lookup for this list. Location-limited access additionally checks for an authorized Log1 posting in each offered period. Entries, totals and revision still use Log1 with unchanged authorization.
Resident balance batches
GetBankUserBalances: POST /api/v1/banks/{bankKid}/users/balances is read-only. Send 1–50 canonical resident KIDs from the same bank, as returned by GetBankUsers. Numeric IDs and readable aliases are rejected. Duplicates appear once, preserving first-requested order.
const response = await fetch(`${apiBase}/api/v1/banks/${bankKid}/users/balances`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ userKids: visibleResidents.map(resident => resident.kid) })
});
if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
const { items } = await response.json();
// Use item.balances: one balance per currency. Never add different currencies together.
Requires an active manager, eTab.Users2, User Read and a bank-wide resource grant. A location-only grant cannot expose a balance across all locations. Permissions are checked on every request. Deleted residents follow the manager's retention rule. Missing or hidden residents return not-found and null, never a fabricated zero.
Use balances, containing one row per currency with currency, currentBalanceMinor, previousBalanceMinor, previousPeriod and previousPeriodIsProvisional. Amounts are signed 64-bit integers in the system's existing minor units (e.g. øre for DKK). No currency conversion occurs; never add different currencies together. Currency codes come from Log1 and are decoded, trimmed and uppercased. Missing/empty codes form a separate currency:null row; no currency is inferred. Rows are sorted by currency code.
The resident discount applies to DKK only. It is added once to the DKK row, which is also created when the resident only has postings in other currencies. An existing resident with no postings and no discount receives balances:[]. Missing/hidden residents also have an empty list, but their status is not-found.
Currency balances use period zero and FlexOrm's missing-settlement correction. Receipt documents are grouped by DocId and their settlement boundary is determined before separating the amounts by currency. Correction history applies tenant limits with positive manager overrides. The provisional-period discount cap uses only its DKK amount. These balances are not an instruction to charge.
Only selected residents are read in shared queries, reusing bank settings within the request, without a shared balance cache. At most two balance batches run per API process, waiting at most one second for capacity, with a 20-second database deadline. Correction history is capped at 50,000 lines per request. The portal resident list renders rows first, then loads current balances in batches of at most 10 residents, one batch at a time. Rows added by scrolling are handled the same way. Balances reload when filtering, sorting or navigating; the list does not poll balances. The API limit remains 50 residents per request.
400: invalid list/KID; 401: expired session; 403: insufficient permission; 413: oversized request. A 503 with storage-busy, storage-timeout, balance-batch-too-large or storage-unavailable returns no partial balances. Reduce oversized batches; back off before a bounded retry after busy/timeout responses. Display failure instead of zero. No database writes are performed.
Existing scalar fields on the resident result, request shape and operation ID remain unchanged for compatibility. The scalars retain the old calculation across currencies and must not be interpreted as one currency's balance; use balances. Both periods and every currency reuse the same Log1 query grouped by resident, period and currency, with no extra query per currency. At most 100 currency buckets are allowed per resident.
All currency rows use the resident's same latest positive period, even if a currency last appeared in an older period. No posting for that currency in the selected period means a balance of 0 for that period. No previous period remains null. Clients should not fall back to the old mixed amounts if the currency list is absent.
The previous period is the resident's highest positive period number, even if the bank has newer periods. A stored period's balance is its sum without adding the discount again. When no previous period exists, both its balance and period number are null, and the provisional flag is false. This includes residents with only period zero or no postings. A previous period with a zero balance instead returns 0 and its period number.
Delayed settlement produces a provisional previous balance using FlexOrm document grouping and capped discount. This also applies to cash banks, whose current balance stays unchanged. previousPeriodIsProvisional=true identifies a period computed only in memory; it has not been stored or settled. Its number is the bank's highest Log1 period plus one and must not be used as an existing settlement/download identifier. That number uses one indexed MAX lookup shared by the batch, only when a correction might be needed. A provisional period can have a zero balance.
GetBankUserBalances also returns latestPostingMs2000 (UTC milliseconds since 2000-01-01; zero means no Log1 postings) and hasActiveSubscription (active card/SEPA subscription). Both fields are null for missing/hidden residents and share the balance permissions and snapshot. Active status follows Orders: CardSubscription or SepaSubscription, Flags > 0, CR2000 > 0 and ActionCode OK/AUTHORIZE. It is not proof of payment. On HTTP 503, show unavailable and retry; never assume inactive or zero. Included in all client variants 0.3.2 for this beta release.
Installers — GetInstallers
GET /api/v1/installers requires an active manager, Installers1 (68), independent PermissionInstaller2.Read and a whole-tenant KID grant. Bank-only or location-only access is insufficient. Account, credential stamp, tab, rights and scope are checked on every page through the bounded manager snapshot. The icon can be changed with SetInstallerIcon as described below.
The response is {items: [...], nextCursor: ...}. Each item contains kid, name, icon, email, locations, tags, deleted, deletedAt, enabled, lastActiveAt. All object identifiers are canonical KIDs; derive the displayed numeric UserId from the installer KID. Location and tag entries contain kid and enum-name state. They retain NoAccess and locked/deleted/history states: they describe the installer, not the caller's permissions. Location names are not looked up.
The trusted site's A{TenantId:D4}.Log7 is read with BankId=TenantId and eUserId.Installeres..InstalleresLast (1–999). Manager authorization settings still use bank zero. No credentials are selected. Disabled installers are included. Deleted rows follow the caller's RetentionDays; invalid/future deletion values are hidden. Missing Deleted means zero. Missing/invalid Enabled is null. Alive uses the current krumb's MS2000, never Text; absent, nonpositive or unrepresentable timestamps are null. UTC timestamps should be displayed in the viewer's time zone. Reads do not update Alive.
pageSize: 1–100, default 50. Follow nextCursor until null, including empty pages. Cursors bind caller, tenant, filter, ordering and page size.
filter: literal Name/Email substring, case-insensitive, trimmed, maximum 128 characters without controls. Escape URL parameters. SQL wildcard characters are literal.
sort: identity (default), name, email, locations, tags, deleted, enabled, lastActive; direction: asc (default) or desc. Ordering applies before paging. Name/email use ordinal case-insensitive comparison. Locations/tags compare sorted numeric ID/state lists lexicographically; deleted compares timestamps; enabled compares null/false/true; activity is chronological. Identity breaks ties in the same direction.
Identity ascending uses bounded keyset reads. Other orderings use a tenant-local index of at most 999 entries, cached for 60 seconds, then fresh page details with retention reapplied. Null sorts first ascending. Changes are not a frozen snapshot; restart if a cursor anchor disappears.
let cursor = null;
do {
const query = new URLSearchParams({pageSize: '50', sort: 'name', direction: 'asc'});
if (cursor) query.set('cursor', cursor);
const response = await fetch(`${api}/api/v1/installers?${query}`, {
headers: {Authorization: `Bearer ${token}`}
});
if (!response.ok) throw new Error(`GetInstallers: ${response.status}`);
const page = await response.json();
renderInstallers(page.items);
cursor = page.nextCursor;
} while (cursor);
Errors: 400 invalid-page/invalid-filter/invalid-sort/invalid-cursor (correct input or restart); 401 (log in again); 403 missing-installers-tab/missing-installers-read/missing-tenant-access (request the missing access); 503 installers-unavailable (storage failure or 12-second deadline, retry manually). Responses are no-store. No partial sorted results, background retry loop or write operations.
Installer details and icon — GetInstaller / SetInstallerIcon
GET /api/v1/installers/{installerKid} requires the same read access as the directory. The response contains installer, canEditIcon, iconRevision and availableIcons. The KID must be a canonical Installer KID for the site's tenant, with BankId equal to TenantId and a UserId in the installer range. Missing or retention-hidden installers return 404.
The icon catalogue uses the same metadata as the manager picker: all eIcon entries with eIconSubject.Person. A valid current icon without Person metadata appears first, but cannot be assigned again. POST /api/v1/installers/{installerKid}/icon requires both Installer Read and Write, tab 68 and whole-tenant access. canEditIcon is only a display hint; the API rechecks current account, rights and retention access during the save.
const detailResponse = await fetch(`${api}/api/v1/installers/${installerKid}`, {
headers: {Authorization: `Bearer ${token}`}
});
if (!detailResponse.ok) throw new Error(`GetInstaller: ${detailResponse.status}`);
const detail = await detailResponse.json();
const response = await fetch(`${api}/api/v1/installers/${installerKid}/icon`, {
method: 'POST',
headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},
body: JSON.stringify({icon: selectedPersonIcon, expectedRevision: detail.iconRevision})
});
if (!response.ok) throw new Error(`SetInstallerIcon: ${response.status}`);
const confirmed = await response.json();
renderInstallerIcon(confirmed.icon); // Apply only after a successful response.
The save changes only eSetting.Icon and returns kid, icon, iconRevision, availableIcons. An unchanged icon produces no history row. Other fields, location access and tags cannot yet be edited here.
400: invalid-installer-kid or invalid-installer-icon. 401 requires a new login. 403: missing-installers-tab, missing-installers-read, missing-installers-write or missing-tenant-access. 404: installer-not-found. 409: installer-icon-conflict; reread before selecting again. 503: installer-icon-unavailable. Keep the previous selection on failure; reread after an uncertain network outcome, and never retry a write automatically.
Administrators — GetManagers
Directory items and GetManager now include operationPermissions and retentionDays for the displayed administrator. The seven categories Managers, Bank, Location, Unit, User, Installer and Service reflect current Log7 PermissionManagers2, PermissionBank2, PermissionLocation2, PermissionUnit2, PermissionUser2PermissionInstaller2 and PermissionService2. The resource, level, flags and six can* fields have the same format as GetCurrentManager. Flags are independent: Write does not imply Read; missing/empty settings default to Read; invalid settings return null flags and no operations. Combine these flags with account state, tabs and Kids.
retentionDays is the displayed administrator’s number of days for viewing otherwise authorized deleted records. Missing, invalid or negative values become 0. It does not schedule physical deletion. The signed-in caller still uses their own permissions and RetentionDays to read the administrator; viewing an account never grants that account’s permissions. Values are read with the other details in the same Log7 query; the sorting index is unchanged. RetentionDays remains read-only; permission editing is documented below. Read status codes are unchanged. Keep using the canonical manager Kid in the curl example below.
GetManager: GET /api/v1/managers/{managerKid} reads one administrator with the fields and authorization requirements of a directory item, plus the availableTabs catalog described below. Use its canonical kid from the list. A different-site KID or one outside the manager range returns 400 invalid-manager-kid. An absent administrator or a record outside retention returns 404 manager-not-found; the next administrator is never substituted. Handle 401, 403 and 503 as for the list. At most one Log7 candidate is read; no database values change.
The portal uses this operation for workspace shortcuts under Administrators. Shortcuts are local to the browser session, tenant and signed-in manager. They never grant access; each visit is rechecked by the API. The administrator overview currently supports reading only.
filter searches the entire administrator list for a case-insensitive substring of Name or Email. Maximum 128 characters; surrounding whitespace is removed. An empty filter lists all permitted entries. Filtering happens before the page limit; %, _ and ! are literal characters. Start without a cursor when changing the filter, and keep the same filter on following pages. Control characters or excessive length return 400 invalid-filter.
Each item also includes organisation, enabled, deleted and deletedAt. Organisation is empty when absent. Enabled is true for 1, false for 0, and null for missing/invalid values. Deleted is true for a positive deletion timestamp; deletedAt is then that UTC timestamp, otherwise null. These fields alone do not prove the account can sign in. Display unknown Enabled as unknown, and continue to enforce the documented permissions. Deleted entries remain limited by the caller’s retention allowance.
GET /api/v1/managers?pageSize=50 reads current tenant Log7 settings, bank zero, in the inclusive eUserId.Managers–eUserId.ManagersLast range. Root, resident and service accounts are outside this range. Items contain kid, name, icon, email, resourceGrants and tabs. Passwords are never returned.
Requires an active manager session, eTab.Managers1 (28), PermissionManagers2 Read and an explicit tenant-wide Kid grant. Bank/location grants are insufficient. Write does not imply Read; absent permission values default to Read. Every page rechecks these requirements using the shared session snapshot, at most 60 seconds old.
let cursor = null;
do {
const url = new URL('/api/v1/managers', API_BASE);
url.searchParams.set('pageSize', '50');
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!response.ok) throw new Error(`GetManagers: ${response.status}`);
const page = await response.json();
renderAdministrators(page.items); // Use textContent, never HTML from data.
cursor = page.nextCursor;
} while (cursor);
For user interfaces, request each following page near the bottom of the list. pageSize is 1–100, default 50, and must remain unchanged. The opaque nextCursor is bound to the tenant, caller, page size, filter, sort and direction. Follow it even after a short or empty page, until null. Restart without a cursor when changing these parameters. Pages are not a frozen snapshot of concurrent changes; if the continuation identity is absent from a refreshed index, 400 invalid-cursor requires restarting the list.
lastActiveAt in both GetManagers and GetManager is the last recorded activity: MS2000 on the current eSetting.Alive Log7 row, BankId 0, converted from milliseconds since 2000-01-01 UTC. It uses the row timestamp, not its Text value or other profile changes. Missing, nonpositive or unrepresentable timestamps return null; display these as unknown. Display valid UTC timestamps in the viewer’s local time zone. Reading does not update Alive, and this value is not proof that the administrator is online. sort=lastActive orders chronologically; unknown times come first ascending and last descending. The sorting index may be up to 60 seconds old while page details are read afresh.
sort=identity|name|email|organisation|kids|deleted|enabled|lastActive and direction=asc|desc order the complete permitted result before paging. The API default remains identity/asc; the portal uses name/asc. Name, email and organisation use case-insensitive ordinal comparison. Kids compare numerically sorted bank/location scopes for the current site, irrespective of stored Log7 order; empty scopes come first. Enabled orders unknown, false, true; Deleted orders by deletion timestamp, with active records first. Manager identity breaks ties; desc reverses the entire order.
Selectable ordering uses a shared seven-setting Log7 index cached for at most 60 seconds per filter. Only the selected page’s details are fetched afresh; access and deletion retention are rechecked each time. Indexes allow at most 20,000 matching identities, within a 50,000-weighted-entry cache (minimum weight 100 per filter). Concurrent index reads are coalesced; data failures delay new index reads for five seconds. A cold cache may need an extra query; the overall request deadline is 12 seconds. Identity/asc retains one bounded query per page. There are no total counts or per-manager queries.
Disabled administrators remain listed. Deleted administrators follow the caller’s RetentionDays; malformed deletion settings are hidden. Site-relative Kids are resolved to the API site, and only valid grants for that site are returned. Tabs follow AttributeMetaSortOrder, then numeric identity. These are assigned scopes and pages, not proof of effective operation permissions.
400 (invalid-page/invalid-cursor/invalid-sort): restart with valid parameters. 401: sign in again. 403: missing-managers-tab, missing-managers-read or missing-tenant-access identifies the missing requirement; do not retry automatically. 503 (managers-unavailable): show an error and offer a bounded retry with the same cursor. 503 manager-directory-too-large: narrow the filter and restart; the API never silently truncates a sorted list. The listing operation only reads data. Permission editing is described below.
Service comes from eSetting.PermissionService2 (3017) in current bank-zero Log7 and is returned by GetCurrentManager, GetManagers and GetManager. It uses the same six independent flags and missing/invalid-value rules. It does not enable service login or grant rights in other categories. Update it through SetManagerPermission with resource Service; existing authorization, concurrency and error handling apply.
Edit administrator permissions
SetManagerPermission: POST /api/v1/managers/{managerKid}/permissions/{resource}. Requires an active session, tab 28, a tenant-wide grant and both PermissionManagers2 Read and Write. Your own permissions are editable only if you are the sole active, non-deleted administrator with whole-tenant access. Every ordinary authorization requirement still applies. GetManager and directory items include isCurrentManager and canEditPermissions as display hints; the API rechecks the current account, credential stamp, tab and access inside the write transaction.
The exception checks other administrators’ Kids, Enabled and Deleted in the tenant’s bank-zero Log7. Only active accounts with an explicit whole-tenant grant count, regardless of their tabs or operation flags. Bank/location-only grants do not count. Directory filters, retention and cached counts never establish the exception. Writes lock the relevant records and ranges in the same serializable transaction as the change; a previously allowed display hint is insufficient. A read response containing your own record and Managers Write needs one extra bounded query for this hint. At most 20,000 other scope records are examined; if the absence of another administrator cannot be confirmed within this bound or the deadline, the request fails with 503.
Read GetManager first. Choose resource: Managers, Installer, Service, Bank, Location, Unit or User. Send one flag (Read=1, Write=2, Create=4, Delete=8, RenameExtrenatId=16, Rename=32), the desired enabled boolean and the category's displayed expectedFlags. The field is required, including explicit null for malformed stored rights. Only the selected bit changes; Write does not imply Read.
200 returns the saved category. Update the checkbox only after a valid OK response. Storage follows theme changes: append bank-zero Log7 history with the caller as actor and verify the current Log7 trigger before commit. Unchanged values append nothing. Tenant comes exclusively from server configuration. The target's local session cache is invalidated; other API instances may retain a read snapshot for up to 60 seconds. Permission writes always use fresh authorization. Disabled targets may be edited; deleted targets follow the caller's RetentionDays.
400: invalid KID, category, flag or input.
401: sign in again.
403: own-manager-permissions when another active administrator also has whole-tenant access, or missing tab, Read, Write or tenant-wide access. Refresh the card; do not retry automatically.
404 manager-not-found: absent or retention-hidden target.
409 permission-conflict: reload and review current permissions before trying again.
503 manager-permissions-unavailable or timeout: retain the displayed checkbox and show an error. A lost response may follow a committed write; reread before manually retrying. Never retry automatically.
A configured write connection and transactional Log7 tables are required. Only the seven Permission*2 settings can be edited here; names, Kids, tabs and other administrator fields are outside this operation.
Send all seven categories in expectedFlags, including Service. Missing or extra categories return 400 invalid-permission-role without writes.
Administrator role presets
SetManagerPermissionRole applies a complete preset in one call: POST /api/v1/managers/{managerKid}/permission-role. It uses exactly the same active-account, Managers1, Managers Read+Write, whole-tenant and own-card exception checks as SetManagerPermission. The API defines the masks; clients send the role identity, current expected masks and the required tab revision.
role
Bank
Location, Unit, User
Managers, Installer, Service
accounting (Regnskab)
Read (1)
Read (1)
None (0)
caretaker (Varmemester)
Read (1)
Read + Write (3)
None (0)
operator (Operator)
All six flags (63)
All six flags (63)
All six flags (63)
For accounting, also send the latest tabsRevision from GetManager as expectedTabsRevision. Existing clients must supply this new property when applying accounting. Missing or invalid revisions return 400 invalid-permission-role. The transaction validates both permissions and tabs before any write. A stale tab revision returns 409 tabs-conflict; malformed stored tabs return 409 invalid-stored-tabs. No part of the role is saved on either conflict. Read again and review before retrying. Set TABS_REVISION below to the revision you read.
Read GetManager first and copy all seven operationPermissions[].flags into expectedFlags, keyed by resource. Include explicit null for an invalid stored mask. The example assumes every currently displayed mask is 1:
The operation replaces all seven categories, including clearing extra flags, in one serializable Log7 transaction. Every expected value is checked before the first write, and each history append records the caller and verifies the current-table trigger before commit. No-op categories append nothing. The response contains role, all seven operationPermissions, tabs, tabsRevision, canEditTabs and canEditPermissions. Validate the complete successful response before updating permissions and tabs together. Use the returned tabsRevision for subsequent edits. Applying accounting or caretaker to yourself removes Managers access and returns canEditPermissions=false; disable further editing. Operator enables all 42 checkboxes, including Managers, Installer and Service, and retains Managers access. Accounting replaces the entire tab selection with exactly Users2 (53), Account2 (2) and Settlement2 (40), removing all other IDs, including unknown stored IDs. Other roles preserve tabs. Kids, account status and profile fields stay unchanged. Roles are one-time presets, not stored memberships or automatic ongoing rules.
400 invalid-permission-role means an unknown role or missing, extra or invalid expected masks; malformed tenant/KID uses invalid-manager-kid. 401 requires login; 403 uses the same access codes as individual changes, including own-manager-permissions; 404 is manager-not-found. A mismatch anywhere returns 409 permission-conflict with no partial save: reload and review. On 503 manager-permissions-unavailable or an uncertain timeout, keep the previous display and reread before manually retrying. Never issue automatic retries or a sequence of individual bit calls as a replacement for the atomic preset. The existing 12-second deadline and sole-manager scan limit apply.
Edit administrator tabs
GetManager additionally returns availableTabs: every recognized numeric eTab except None and Length, ordered by AttributeMetaSortOrder then ID, with aliases deduplicated. Each item contains id, enum-derived name and iconKid. This catalog includes pages not yet implemented. Mark entries whose IDs appear in tabs. The directory omits the catalog; both reads return canEditTabs and the opaque tabsRevision.
SetManagerTab: POST /api/v1/managers/{managerKid}/tabs/{tabId}. Requires an active account, Managers1 (28), independent Managers Read and Write, and a whole-tenant grant. Own edits use the same sole active tenant-wide manager exception as permission changes. Display hints never authorize a write: the API rechecks the locked current account, tab, scope and permissions inside the transaction.
Read the target first, copy its exact tabsRevision, and choose an ID from availableTabs. The example grants tab 8:
TABS_REVISION='copy tabsRevision from GetManager'
curl -X POST "$API_BASE/api/v1/managers/$MANAGER_KID/tabs/8" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data "{\"enabled\":true,\"expectedRevision\":\"$TABS_REVISION\"}"
200 returns the saved tabs, next tabsRevision, canEditTabs and canEditPermissions. Update selection only after validating the successful response and use its revision for the next click. Removing your own Managers1 grant returns both edit hints false: lock all editor controls. Tab access is only one requirement; it does not grant Kids, account access or operation rights.
Only the requested tab changes. Unknown integer IDs and unrelated stored entries are preserved. The change uses the shared serializable bank-zero Log7 history transaction, caller attribution and verified current-table trigger; a no-op adds no history. Actor and target session caches are invalidated even on uncertain outcomes. Other instances may retain read snapshots for up to 60 seconds; every write rechecks current authorization. The 12-second deadline and sole-manager scan limit still apply.
400 invalid-manager-kid or invalid-tab-change: invalid/foreign KID, unavailable tab, missing boolean or invalid revision.
401: sign in again. 403: the same access codes as permission changes, including own-manager-permissions. 404: manager-not-found.
409 tabs-conflict: reread and review the latest selection. invalid-stored-tabs: stored JSON is not an integer array; no data was overwritten.
503 manager-tabs-unavailable, timeout or a lost response: retain the displayed selection and reread before a manual retry. Never retry writes automatically.
Administrator Kids
GetManager and GetManagers return resourceGrants, kidsRevision and canEditKids. Use SearchBanks and SearchLocations to find choices; their normal Read, scope and retention rules still apply. Residents and units are not valid resource grants.
SetManagerKid: POST /api/v1/managers/{managerKid}/kids. Requires an active manager, Managers1 (28), independent Managers Read and Write, and explicit whole-tenant access. Own edits require being the sole active tenant-wide manager. The API rechecks current Log7 snapshots inside the transaction.
A Bank KID grants the entire bank and replaces its individual location grants.
A Location KID grants only that exact location.
The site’s Tenant KID means All banks and replaces individual bank/location selections. Removing it later does not restore those selections.
enabled: false removes only the exact grant. Already covered additions are no-ops.
A confirmed 200 response returns the complete resourceGrants, next kidsRevision and canEditKids. Update selections only after confirmation. Removing your own tenant grant returns canEditKids=false; lock the entire editor. KIDs must be canonical and belong to the site tenant; clients cannot select a tenant. Added banks/locations must exist as exact Settings scopes in Log24. Removing stale grants is allowed.
Only Kids change through the existing audited bank-zero Log7 transaction. Tabs and other permissions are preserved. Stored site-relative grants resolve to this site; foreign stored entries are preserved without granting access here. Growth is limited to 1,000 Kids; removals and broader bank replacements remain allowed. Session and directory caches are invalidated; read caches on other API instances may live for up to 60 seconds. Every write reauthorizes.
400 invalid-manager-kid/invalid-kid-change: invalid/foreign identity, unsupported scope or missing boolean/revision.
401: sign in. 403: normal permission codes, including own-manager-permissions. 404 manager-not-found includes retention-hidden managers; resource-not-found means the selected bank/location does not exist.
409 kids-conflict: reread and review. invalid-stored-kids: invalid stored grants were not overwritten. kids-limit: too many grants.
503 manager-kids-unavailable, timeout or a lost response: keep the display, reread before a manual retry and never retry automatically. Writes have a 12-second deadline.
Deletion retention during a write and the confirmed canEditProfile hint use the same database clock as the deletion timestamp. An authorized caller can immediately restore another manager within their existing retention window, even when the API and database clocks differ slightly. Restore sends Deleted=false with the latest profileRevision; update the toggle only after a valid 200 response.
Edit administrator details
SetManagerProfileField: POST /api/v1/managers/{managerKid}/profile/{field}. Requires an active manager, Managers1 (28), Managers Read and Write and whole-tenant access. Own edits use the same sole active tenant-wide manager exception as permissions and tabs. These seven fields use Managers Write, without additional Delete or Rename flags. Every write rechecks the locked current authorization snapshot and target visibility.
First read GetManager. Its canEditProfile is a display hint; copy profileRevision exactly. Use the case-sensitive field names below:
field
JSON value
Stored meaning
Name / Organisation
String, up to 200 characters, no controls
Unicode/whitespace preserved; empty clears the field.
Enabled
true / false
1 / 0
Deleted
true / false
Server deletion time in MS2000 / 0
RetentionDays
Integer 0–2147483647
Days of visibility for otherwise authorized deleted records.
Icon
Exact eIcon name as a string
Only icons with eIconSubject.Person metadata may be newly assigned.
Email
One nonempty email address, up to 254 characters
Login address. No whitespace, controls or display-name syntax. Case is preserved.
Email format is validated before any database access: a plain ASCII internet address with at most 64 characters before @ and a fully qualified domain such as example.com. Domain labels contain 1–63 letters, digits or hyphens, with no leading/trailing hyphen. International domains use punycode. Empty addresses, name@company, repeated/leading/trailing dots, whitespace, quoted local parts and address literals return 400 invalid-profile-change. The stored address and revision remain unchanged. The portal shows a field error without sending a save request; a corrected address saves on the next blur. Other profile edits preserve existing legacy email values. This checks format, not mailbox existence.
Email edits use the same authorization and revision checks. The response includes the confirmed email. The password and existing sessions remain unchanged. The address is not verified and no email or invitation is sent by this edit. If multiple accounts later match the same email and password at login, the active, non-deleted manager with the newest Alive krumb MS2000 is retained and the other matching passwords are cleared as described in the login section. Invitations use the separate InviteManager operation.
GetManager.availableIcons contains all Person icons in numeric enum order. Mark the current iconKid. If it is outside the Person catalog, it appears first for display only; it cannot be newly assigned. Missing/unsafe filenames display as user without rewriting storage. The paged directory omits the catalog. Successful saves return iconKid and updated availableIcons. Change the selection only after a valid 200 response; preserve it on failure. Icon edits share revision protection and authorization with the other profile fields.
For deletion send {"value":true,"expectedRevision":"..."} to the /profile/Deleted endpoint. To restore, send false with the latest revision. The client never sends a deletion timestamp. The database server sets milliseconds since 2000-01-01 UTC; this is not Unix time. Repeating true preserves the first deletion timestamp. Restoration stores 0, never a boolean string. Only the selected field changes.
200 returns field, the seven profile values, deletedMs2000 (0 or timestamp), deletedAt (UTC or null), a fresh profileRevision and canEditProfile. Validate the complete response before updating the UI. False editability locks profile, permissions, role and tab controls. It occurs when disabling/deleting yourself or when deleting another manager hides them under your RetentionDays. Restore requires a caller whose existing retention includes that deleted manager; the write never bypasses retention.
The shared serializable bank-zero Log7 writer appends only changed settings, attributes the caller and verifies the current-value trigger before committing. It does not write legacy tables or alter passwords. Session and directory caches on this instance are invalidated even after an uncertain outcome; other instances can retain read snapshots for up to 60 seconds. Disabled/deleted accounts lose access at their next fresh authorization check; writes always reauthorize immediately. The 12-second deadline and sole-manager scan limit apply.
400 invalid-manager-kid or invalid-profile-change: wrong identity, unknown field, invalid type/range or missing revision/value.
401: sign in. 403: normal permission/own-card denial. 404 manager-not-found: absent or outside caller retention.
409 profile-conflict: one of the seven raw profile fields changed; reread and review.
503 manager-profile-unavailable or timeout: keep the previous state and unsaved drafts. A lost response may follow commit, so reread before a manual retry; never automatically retry.
Kombine.Flex.Portal.Client provides typed C# methods for all 100 public integration operations, including anonymous public calls. One NuGet package contains .NET Standard 2.0, .NET 8 and .NET 10 assets. It has no dependencies on other Kombine packages or projects and contains no database access, KID calculation or business rules. Production releases are published on NuGet.org after deployment verification. If this beta version is not yet listed there, use the direct package download and local installation below.
Platforms and dependencies
Consumer platform
Client asset selected by NuGet
Dependencies
.NET Framework 4.7.2 / 4.8 / 4.8.1
.NET Standard 2.0
Microsoft System.Text.Json 10.0.12 and its Microsoft dependencies
.NET 8 / 9
.NET 8
No additional packages
.NET 10
.NET 10
No additional packages
Checks cover builds against Framework 4.7.2 and 4.8, running on installed Framework 4.8.1, plus execution on .NET 8, 9 and 10. An original 4.7.2 installation and older .NET Core/.NET versions have not been exercised. Microsoft recommends Framework 4.7.2 or later for .NET Standard. Customers without NuGet can still use the separate DLL packages for 2.0, 4.5 and CE/Mobile.
First select the tenant API URL, then enter e-mail and password. The client uses existing API operations; API functionality and permission enforcement are unchanged.
Installation
Production releases are published on NuGet.org after deployment verification. If this beta version is not yet listed there, use the direct package download and local installation below.
Keep nuget.org or your approved mirror enabled to restore Microsoft dependencies. In Visual Studio, add the same local directory as a NuGet source. Framework executables should enable automatic binding redirects when dependency versions conflict; applications injecting their own HttpClient also need the framework System.Net.Http reference.
Login and tabs
using Kombine.Flex.Portal.Client;
// Inside an async method; tenantApiUrl ends in /
using (var api = new PortalApiClient(new Uri(tenantApiUrl)))
{
await api.LoginAsync(email, password, cancellationToken);
var manager = await api.GetCurrentManagerAsync(cancellationToken);
if (manager.TabDetails != null)
foreach (var tab in manager.TabDetails)
Console.WriteLine($"{tab.Id}: {tab.Name}");
api.ClearSession();
}
Methods follow OperationIdAsync with typed request/response models and IntelliSense: for example, GetBankUsersAsync, GetBankUserBalancesAsync and GetInstallersAsync. Send canonical KIDs and cursors unchanged. The client grants no additional permissions and performs no automatic retries.
PortalApiException.StatusCode and Code describe API failures: 401 requires login, 403 denies access, 409 requires reloading the revision, 429 requires respecting Retry-After, and 503 means temporarily unavailable. Network failures use HttpRequestException. Never indiscriminately log passwords, tokens or entire responses.
ClearSession forgets the client's token; packaged clients do not renew automatically, and the API has no server-side logout operation. Switching tenant requires a new client and separate login. The separate console and Windows examples use only this client for API access. The Windows example displays permitted tabs and stores only token/expiry in Windows Credential Locker.
API contract changelog · Review request/response changes before updating your integration.
Python
Python 3.11+ · pip / wheel · 100 API-operations
kombine-flex-portal-client is a standalone Python 3.11+ package using only the standard library. No .NET, NuGet or other Kombine packages are needed.
Installation
Install the supplied wheel file locally. The package is not yet published to PyPI.
First select the tenant API URL ending in /, then email and password. Create a new client when switching tenants. The API enforces account, Tab, KID and operation permissions for every call. See access rules and error codes.
# Python: tenant_api_url, email and password come from your login form.
from kombine_flex_portal import PortalClient, PortalApiError
with PortalClient(tenant_api_url) as api:
try:
api.login(email, password)
manager = api.get_current_manager()
for tab in manager.get("tabDetails") or []:
print(tab.get("id"), tab.get("name"))
except PortalApiError as error:
print(error.status, error.code)
login() retains the token in memory. The raw login_manager(body) call does not retain a session. clear_session() and close() forget the local token; the API does not yet provide refresh or token revocation. Public calls never send the token. The client does not retain the password.
Operations and data types
All 100 operations have snake_case methods, such as get_bank_user_balances and get_installers. Request and response models are TypedDict in kombine_flex_portal.models. JSON field names, KIDs, cursors and revisions are preserved. Int64 uses exact Python integers; ISO dates remain strings. Null is not zero.
with api.export_bank_users(bank_kid) as download:
with open("residents.csv", "wb") as target:
for chunk in download.iter_bytes():
target.write(chunk)
PortalApiError exposes status, code, headers and response. The response may contain personal data. Network failures use URLError/OSError/TimeoutError; invalid or oversized JSON raises PortalProtocolError. No automatic retries or pagination.
The client is synchronous. The default timeout=30 is a socket I/O timeout, not an overall deadline for a long download. JSON defaults to a 16 MiB limit. Use a worker thread in async/GUI applications. HTTPS uses normal certificate validation; redirects are refused.
API contract changelog · Review request/response changes before updating your integration.
@kombine/flex-portal-client is one ESM package containing JavaScript and TypeScript declarations. It works in Node.js 22+ and modern browsers with fetch, BigInt and Web Streams. There are no runtime dependencies or other Kombine packages.
Installation
Install the supplied tarball. The package is not yet published to npm. TypeScript is optional; compiled JavaScript is already included.
First select the tenant API URL ending in /, then email and password. Create a new client when switching tenants. The API enforces account, Tab, KID and operation permissions for every call. See access rules and error codes.
import { PortalClient, PortalApiError } from '@kombine/flex-portal-client';
const api = new PortalClient(tenantApiUrl);
try {
await api.login(email, password);
const manager = await api.getCurrentManager();
for (const tab of manager.tabDetails ?? []) console.log(tab.id, tab.name);
} catch (error) {
if (error instanceof PortalApiError) console.error(error.status, error.code);
else console.error('The API request could not be completed.');
} finally { api.clearSession(); }
login() retains the token in memory. The raw loginManager(body) call does not retain a session. clearSession() and close() forget the local token; the API does not yet provide refresh or token revocation. Public calls never send the token. The client does not retain the password.
Operations and data types
All 100 operations have camelCase methods and exported TypeScript interfaces. Int64 fields use bigint, including expiresIn, so large balances are not rounded. Int32 uses number; dates remain ISO strings. KIDs, cursors, revisions and null are preserved.
Use value.toString() to display bigint. JavaScript JSON.stringify cannot directly handle bigint; explicitly choose a representation such as strings for your own JSON output. The client serializer sends int64 correctly as JSON numbers. Do not blindly convert large amounts to Number.
PortalApiError exposes status, code, headers and response; entire responses may contain personal data. Network/CORS failures use fetch errors, cancellation/timeouts normally use AbortError/TimeoutError, and invalid JSON raises PortalProtocolError. No automatic retries or pagination.
The default timeoutMs: 30_000 covers the entire response, including streaming. Each call accepts AbortSignal. JSON defaults to a 16 MiB limit; file downloads are streamed and must be closed. HTTPS uses normal certificate validation, and redirects are refused.
Browser / CORS
Use a bundler, or host the entire package dist directory on your webserver and import ./dist/index.js from a <script type="module">. The API Cors:AllowedOrigins must allow the page’s exact origin. Browsers can only read CORS-exposed response headers. Node.js does not require browser CORS.
Customers with legacy .NET Framework 2.0 applications can reference Kombine.Flex.Portal.Client.Net20.dll using Add Reference → Browse. The ZIP includes the DLL, XML IntelliSense documentation, source, Kombine.Flex.Portal.Client.2008.sln and a standalone test application. No NuGet or Kombine packages are needed; only mscorlib 2.0 and System 2.0. This targets .NET Framework 2.0, not .NET Standard 2.0.
Installation
Extract the ZIP and add the client DLL using Add Reference → Browse. Keep the supplied XML file beside the DLL for IntelliSense. NuGet is not required.
Login and tabs
Place using/Imports directives at the top of the file and the remaining code inside a method. Input variables (tenantApiUrl, credentials and KIDs) come from your application. The examples use the same DLL in both languages.
C#
using System;
using Kombine.Flex.Portal.Client.Net20;
using (PortalApiClient api = new PortalApiClient(new Uri(tenantApiUrl)))
{
ManagerSessionResponse session = api.Login(email, password);
ManagerProfileResponse manager = api.GetCurrentManager();
Console.WriteLine(manager.Name);
if (manager.TabDetails != null)
{
foreach (ManagerTabResponse tab in manager.TabDetails)
Console.WriteLine("{0}: {1}", tab.Id, tab.Name);
}
api.ClearSession();
}
VB.NET
Imports System
Imports Kombine.Flex.Portal.Client.Net20
Using api As New PortalApiClient(New Uri(tenantApiUrl))
Dim session As ManagerSessionResponse = api.Login(email, password)
Dim manager As ManagerProfileResponse = api.GetCurrentManager()
Console.WriteLine(manager.Name)
If manager.TabDetails IsNot Nothing Then
For Each tab As ManagerTabResponse In manager.TabDetails
Console.WriteLine("{0}: {1}", tab.Id, tab.Name)
Next
End If
api.ClearSession()
End Using
Select the tenant API URL first. All 100 public integration operations have synchronous methods without an Async suffix. Optional filters use OperationOptions classes, such as GetBankUsersOptions. Dates/timestamps are ISO 8601 strings preserving offsets; money in minor units uses nullable Int64. Keep KIDs/cursors/revisions unchanged. The library contains no database access or business rules.
API errors use PortalApiException with StatusCode, Code and Headers; follow the same 400/401/403/404/409/429/503 rules described above. Network/TLS/timeouts use WebException. There are no automatic retries. Calls block and should run on a background thread in desktop UIs. ClearSession forgets the local token; use RenewManagerSession explicitly for renewal; individual token revocation is unavailable.
HTTPS requirements: .NET 2.0-compatible code still needs a patched Windows/CLR 2.0 installation supporting TLS 1.2 and trusted certificates. The library does not change machine settings. Explicitly calling PortalApiClient.EnableTls12() selects TLS 1.2 for the entire host process and fails if unsupported. See Microsoft's TLS guidance. No fallback disables certificate validation or sends customer data over insecure HTTP.
API contract changelog · Review request/response changes before updating your integration.
Kombine.Flex.Portal.Client.Net45.dll exposes the same 105 typed synchronous API operations. The ZIP includes DLL/XML, source, Kombine.Flex.Portal.Client.2012.sln and a standalone test application. Only framework libraries are required; no NuGet or Kombine dependencies. VS2008 supports up to .NET Framework 3.5; 4.5 requires VS2012 or compatible build tools. See Microsoft's version overview.
Installation
Extract the ZIP and add the client DLL using Add Reference → Browse. Keep the supplied XML file beside the DLL for IntelliSense. NuGet is not required.
Login and tabs
Place using/Imports directives at the top of the file and the remaining code inside a method. Input variables (tenantApiUrl, credentials and KIDs) come from your application. The examples use the same DLL in both languages.
C#
using System;
using Kombine.Flex.Portal.Client.Net45;
using (PortalApiClient api = new PortalApiClient(new Uri(tenantApiUrl)))
{
ManagerSessionResponse session = api.Login(email, password);
ManagerProfileResponse manager = api.GetCurrentManager();
Console.WriteLine(manager.Name);
if (manager.TabDetails != null)
{
foreach (ManagerTabResponse tab in manager.TabDetails)
Console.WriteLine("{0}: {1}", tab.Id, tab.Name);
}
api.ClearSession();
}
VB.NET
Imports System
Imports Kombine.Flex.Portal.Client.Net45
Using api As New PortalApiClient(New Uri(tenantApiUrl))
Dim session As ManagerSessionResponse = api.Login(email, password)
Dim manager As ManagerProfileResponse = api.GetCurrentManager()
Console.WriteLine(manager.Name)
If manager.TabDetails IsNot Nothing Then
For Each tab As ManagerTabResponse In manager.TabDetails
Console.WriteLine("{0}: {1}", tab.Id, tab.Name)
Next
End If
api.ClearSession()
End Using
Select the tenant API URL before login. The same account/Tab/KID/operation permissions and error codes apply. PortalApiException exposes HTTP status, code and headers; network/TLS/timeout failures use WebException or I/O exceptions. Invalid/oversized JSON raises InvalidDataException. There are no automatic retries or token refresh. The JSON limit is 16 MiB; downloads are streamed and must be disposed. Dates remain ISO 8601 strings and balances nullable Int64. The explicit TLS helper changes process-wide protocol selection, not certificate checks or the registry. Builds are checked against 4.5 reference assemblies; runtime checks use a newer installed CLR 4, not an original 4.5 installation.
API contract changelog · Review request/response changes before updating your integration.
Windows CE / Windows Mobile: Compact Framework 2.0
Use Kombine.Flex.Portal.Client.Compact20.dll in Smart Device projects. The separate Kombine.Flex.Portal.Client.Compact2008.sln and ZIP include all 105 typed synchronous operations, source and an on-device test application. Only Compact Framework libraries are required; no NuGet or Kombine dependencies. The desktop client DLL is not a substitute.
Installation
Extract the ZIP and add the client DLL using Add Reference → Browse. Keep the supplied XML file beside the DLL for IntelliSense. NuGet is not required.
Login and tabs
Place using/Imports directives at the top of the file and the remaining code inside a method. Input variables (tenantApiUrl, credentials and KIDs) come from your application. The examples use the same DLL in both languages.
C#
using System;
using Kombine.Flex.Portal.Client.Compact20;
using (PortalApiClient api = new PortalApiClient(new Uri(tenantApiUrl)))
{
api.TimeoutMilliseconds = 30000;
ApiStatusResponse status = api.GetPortalStatus();
ManagerSessionResponse session = api.Login(email, password);
ManagerProfileResponse manager = api.GetCurrentManager();
Console.WriteLine(manager.Name);
if (manager.TabDetails != null)
{
foreach (ManagerTabResponse tab in manager.TabDetails)
Console.WriteLine("{0}: {1}", tab.Id, tab.Name);
}
api.ClearSession();
}
VB.NET
Imports System
Imports Kombine.Flex.Portal.Client.Compact20
Using api As New PortalApiClient(New Uri(tenantApiUrl))
api.TimeoutMilliseconds = 30000
Dim status As ApiStatusResponse = api.GetPortalStatus()
Dim session As ManagerSessionResponse = api.Login(email, password)
Dim manager As ManagerProfileResponse = api.GetCurrentManager()
Console.WriteLine(manager.Name)
If manager.TabDetails IsNot Nothing Then
For Each tab As ManagerTabResponse In manager.TabDetails
Console.WriteLine("{0}: {1}", tab.Id, tab.Name)
Next
End If
api.ClearSession()
End Using
Select the tenant HTTPS API URL first. Login, tabs, KIDs and operation permissions follow the rules above. API errors use PortalApiException; network failures use WebException or I/O exceptions. Invalid/oversized JSON raises PortalProtocolException. The JSON limit is 2 MiB; use small pages. Downloads are streamed. The overall HTTP deadline defaults to 30 seconds and is configurable. There are no automatic retries or token refresh.
Device requirements: CF 2.0 compatibility does not guarantee modern HTTPS. TLS, cipher suites and certificate support depend on the device OS/OEM image. There is no EnableTls12() in the Compact client, and it does not upgrade the networking stack. Test GetPortalStatus() on the actual device before login. No insecure fallback or changes to API security are made. The library and device tests build against CF 2.0; runtime/HTTPS on a physical CE/Mobile device or emulator has not yet been verified.
PHP · Portal API
64-bit PHP 8.2+, ext-curl and ext-json. No additional PHP libraries. Version 0.3.2 is included in the API downloads. It is not published to Packagist.
Version 0.3.1 synchronizes GetBankUserBalances documentation with its 20-second database deadline. Request and response fields are unchanged. Allow extra time for transport and authorization; HTTP 503 still returns no partial balances. Version 0.2.5 adds optional latestPostingMs2000 and hasActiveSubscription fields to GetBankUserBalances. The posting time is a 64-bit UTC millisecond count since 2000-01-01; zero means no postings. Null or an absent field means unknown, and missing or hidden residents return null. Subscription status is not payment confirmation. Keep existing balance handling and permissions; see /docs#user-balances. Version 0.2.5 adds GetLocationOpeningHours and GetLocationBookingRules. Both require Location Read, Unit Read and the authorized location scope. Reservation rules contain plain text plus ordered parts with text/isValue for optional value emphasis; never render these strings as HTML. Keep text as the fallback for older responses. See /docs#location-opening-hours and /docs#location-booking-rules for permissions, examples and limits. Version 0.2.5 adds GetUserReceipts and GetHostingMetrics for API releases that expose these operations. Load receipts on demand, starting at offset 0. Continue with nextOffset and the same revision; on HTTP 409 (receipts-changed), discard earlier pages and restart at offset 0. Keep currencies separate and minor-unit amounts as 64-bit integers. See /docs#user-receipts and /docs#hosting for permissions and limits.
Without Composer: extract to flex-portal-client/ and require its autoload.php instead. Keep the complete src/ folder. The ZIP includes English, Danish and Spanish READMEs, operation and model references, and the public OpenAPI snapshot.
Use a manager email and password for the tenant Portal API. Supply tenantUrl and the login variables from protected application configuration or a login form. The API URL must end with /. Keep the bearer on the PHP server.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Kombine\Flex\Portal\PortalClient;
use Kombine\Flex\Portal\ApiException;
use Kombine\Flex\Portal\ProtocolException;
use Kombine\Flex\Portal\TransportException;
$api = new PortalClient($tenantUrl, timeout: 30);
try {
$session = $api->login($email, $password);
$result = $api->getCurrentManager();
} catch (ApiException $error) {
$status = $error->status;
$code = $error->apiCode;
$retryAfter = $error->headers['retry-after'] ?? null;
// Handle according to the table below; do not blindly repeat writes.
} catch (TransportException | ProtocolException $error) {
// Timeout, network failure, malformed data or response limit.
// A write may already have completed: check state before repeating it.
} finally {
$api->close();
}
renew() explicitly calls RenewManagerSession and stores the replacement token. Raw renewManagerSession() only returns the response. Renew while the bearer is unexpired, based on user activity; after expiry or revocation, log in again. There is no separate refresh token.
Every business call still requires the manager’s account state, permitted Tab, KID scope and operation permission. A KID itself grants no access.
Paging is explicit: keep the returned cursor and continue until it is absent, including after an empty page. Keep revision tokens for writes. Use a temporary file for downloads and rename it only after success.
$page = $api->getBankUsers($bankKid, ['pageSize' => 25, 'sort' => 'number']);
$balances = $api->getBankUserBalances($bankKid, ['userKids' => $userKids]);
$receipts = $api->getUserReceipts($userKid, ['offset' => 0]);
// Request the next page only when needed, using nextOffset and the same revision.
$stream = fopen($temporaryPath, 'w+b');
try {
$download = $api->exportBankUsers($bankKid, $stream);
} finally {
fclose($stream);
}
rename($temporaryPath, $completedPath);
ApiException exposes status, apiCode and lowercase headers. 400: correct input; 401: sign in; 403: check permissions; 404/409: reload and resolve conflicts; 429: respect Retry-After; 5xx: handle temporary failure. TransportException and ProtocolException mean network/deadline or invalid/oversized data. A failed write may have completed: check state before retrying. Never log credentials, tokens or complete payloads.
Responses use associative arrays; lists use indexed arrays. Use native 64-bit int for integer fields; preserve missing/null values. The client verifies TLS, refuses redirects, omits tokens on anonymous calls and clears the session on HTTP 401. Default limits: 30 seconds total, 16 MiB JSON, 64 KiB headers, 1 GiB per download. Downloads write to a caller-owned stream; discard partial files on failure. No automatic retries, paging, renewal or acknowledgements. Server-side PHP does not need browser CORS.
Changelog
Changes to existing endpoints’ request or response contracts that may require changes in your integration. Internal fixes and improvements that keep the contract compatible are not listed.
Each entry identifies the endpoint, the previous and new contract, the customer action and the release status. Release labels describe the version served by this host. Beta and production roll out independently; check the documentation on your target host before migrating.
Tracking starts on 27 September 2026. Earlier releases have not been backfilled.
Release 2026-10-02 · available on this host · clients 0.3.1
Compact reservation rules, complete role requests and canonical map route
GetLocationBookingRules — GET /api/v1/locations/{locationKid}/booking-rules: previously each groups entry contained a complete policy for units sharing calendar/settings. Now groups contain the compact presentation: identical rules are combined; shared rules appear once in a section with common:true, localized name and optional help, followed by differences. Each section identifies its applicable units. Render groups in order and combine applicable common and specific rules when evaluating the displayed policy for a unit. Do not treat a differences section as a complete policy or combine reservation quotas. The development-only displayGroups field is removed; use groups.
SetManagerPermissionRole — POST /api/v1/managers/{managerKid}/permission-role: expectedFlags must include all seven categories. Previously omitting only Service preserved its stored value; now incomplete requests return 400 invalid-permission-role without writes. Read the current matrix and send Managers, Installer, Service, Bank, Location, Unit and User.
GetPublicDisp73 — GET /api/v1/public/displays/Map1: the old /api/v1/public/displays/disp73 alias is removed and returns 404. Update stored URLs to Map1. The operation ID and canonical route response are unchanged. UserBalance contracts are unchanged.
Release 2026-10-01 · available on this host
Presentation response fields renamed to IconKid
Breaking response change:icon, bankIcon and unitIcon become iconKid, bankIconKid and unitIconKid, including nested objects, navigation, tabs, audit editors and document columns. They remain JSON strings. Previously an enum name; now an API-computed icon identity. Only Kid.Icon returns eIcon.ToString(); extra text/count/colour/icons return canonical Kid.ToString(). Empty/unavailable metadata remains an empty string. Object numbers are embedded in Text; calendar Text is the current day of month in Europe/Copenhagen. Rendering an existing filename does not consult the clock.
Stable operation ID
Unchanged method/path
GetCurrentManager
GET /api/v1/session/me
GetMyManagerProfile
GET /api/v1/session/me/profile
SetMyManagerProfileField
POST /api/v1/session/me/profile/{field}
GetManagers
GET /api/v1/managers
GetManager
GET /api/v1/managers/{managerKid}
SetManagerProfileField
POST /api/v1/managers/{managerKid}/profile/{field}
SetManagerTab
POST /api/v1/managers/{managerKid}/tabs/{tabId}
SetManagerPermissionRole
POST /api/v1/managers/{managerKid}/permission-role
GetInstallers
GET /api/v1/installers
GetInstaller
GET /api/v1/installers/{installerKid}
SetInstallerIcon
POST /api/v1/installers/{installerKid}/icon
GetServices
GET /api/v1/services
GetService
GET /api/v1/services/{serviceKid}
SetServiceProfileField
POST /api/v1/services/{serviceKid}/profile/{field}
GenerateServiceApiKey
POST /api/v1/services/{serviceKid}/api-key
GetLocations
GET /api/v1/locations
GetBankLocations
GET /api/v1/banks/{bankKid}/locations
GetLocationUnits
GET /api/v1/locations/{locationKid}/units
GetUnitOverview
GET /api/v1/units/{unitKid}
GetUnitGroup
GET /api/v1/units/{unitKid}/groups/{kind}/{group}
SetUnitSetting
POST /api/v1/units/{unitKid}/groups/settings/{group}/{setting}
GetUnitSettingHistory
GET /api/v1/units/{unitKid}/groups/settings/{group}/{setting}/history
GetBankUsers
GET /api/v1/banks/{bankKid}/users
GetBankUserWorkspace
GET /api/v1/banks/{bankKid}/users/{userKid}/workspace
CreateBankUser
POST /api/v1/banks/{bankKid}/users
ExecuteBankUserCommand
POST /api/v1/banks/{bankKid}/users/{userKid}/commands
GetBankBookings
GET /api/v1/banks/{bankKid}/bookings
ExecuteBankBookingCommand
POST /api/v1/banks/{bankKid}/bookings/{bookingKid}/commands
GetBankAccount
GET /api/v1/banks/{bankKid}/account
GetBankDocuments
GET /api/v1/banks/{bankKid}/documents
GetUnitDocumentTable
GET /api/v1/documents/{documentKid}/table
GetTenantStatus
GET /api/v1/tenant/status
SearchBanks
GET /api/v1/search/banks
SearchBankActivation
GET /api/v1/search/bank-activation
GetSearchBank
GET /api/v1/search/banks/{bankKid}
SearchLocations
GET /api/v1/search/locations
SearchLocationActivation
GET /api/v1/search/location-activation
SearchUsers
GET /api/v1/search/users
SearchUserSms
GET /api/v1/search/user-sms
SearchUserActivation
GET /api/v1/search/user-activation
Migration: rename response model properties and pass the supplied string unchanged, URL-encoded, to /api/v1/icon/{iconSet}/{kid}.svg. Stop interpreting every value as an enum name or composing KIDs in the portal. Use public GetIconPresentation — GET /api/v1/icon/presentation for presentation options and a fresh calendar identity. Icon setting writes and availableIcons still use enum names; service objects also expose iconName for selection. GetActiveLocationCount (GET /api/v1/locations/active-count) additionally returns a ready-to-render iconKid with its badge. Permissions and tenant binding are unchanged; these values grant no access.
Release: 2026-10-01, client version 0.2.1. Generated clients, OpenAPI snapshots and downloadable packages are synchronized with this contract. This release label applies to the version served by this host; beta and production are promoted independently. Existing image paths and image caching are unchanged.
Release 2026-10-01 · available on this host
Icon images · Canonical KID with icons, count, color and text; required asset set
Affected requests:kid previously accepted an eIcon name, numeric value/index or substring. It now first parses a canonical Kombine.Flex.Kid.ToString() and reads Kid.Icons, Kid.Count (Int64), Kid.Color and Kid.Text. Separate count/color/text/sub path fields are removed. Color uses the low 24 bits as opaque RGB (0 = black; the high byte is ignored, as for Flex eColor). Text is UTF-8, case-sensitive and limited to 128 characters without controls. Encoded canonical KIDs are limited to 2048 characters. If that fails, an exact case-insensitive eIcon name is accepted with count zero, black and empty text. Numeric/index/substring enum lookup is no longer a fallback; invalid input returns 400. The first list entry is the main icon and the second is the under-icon. A missing/none second entry means no under-icon; later entries do not affect rendering. Every list entry must be a defined eIcon value. An empty list uses eIcon.none. Other KID fields cause no business lookup or permission change. Count ≤ 0 hides the badge. Positive counts display in full in a red capsule whose straight middle widens between circular ends. Very long labels widen the SVG canvas; no count is abbreviated.
Operation ID
Previous method/path
New method/path
GetIconFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GET /api/v1/icon/{iconSet}/{kid}.{format}
GetIconImageFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{backColor}/{size}.{format}
GetIcon2Svg (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
Use GetIconFromSet with explicit line or g.
GetIcon2Image (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
Use GetIconImageFromSet with explicit line or g.
GetIcon2ImageWithBackground (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Use GetIconImageWithBackgroundFromSet with explicit line or g.
Customer action
The interim local route /api/v1/icon/{iconSet}/{kid}/{sub}.{format} is also replaced by /api/v1/icon/{iconSet}/{kid}.{format}. Append the former sub value as the second entry of Kid.Icons and remove its path segment; the size/background forms remove that segment in the same way.
Add the main icon and optional under-icon to Kid.Icons in that order, set Count, Color and Text on a Kid, URL-encode ToString(), remove the count/color/text/sub segments and select the set explicitly (line preserves the former default). For example, Kid.Icons = [house, check], Count = 7, Color = 0x336699, Text = "A12" produces 413132x7qE20i11Bi336699Ic: use /api/v1/icon/line/413132x7qE20i11Bi336699Ic.svg. For count zero, black and empty text, /api/v1/icon/line/house.svg also works. Update stored links/builders; old shapes are not aliases and overlapping paths can be interpreted as different requests. Rendering formats, set fallback, cache/304 behavior and the public asset catalog remain unchanged. Unknown sets/missing assets return 404; invalid parameters return 400.
Release: 2026-10-01, client version 0.2.1. Generated clients, OpenAPI snapshots and downloadable packages are synchronized with this contract. Upgrade the client and migrate the removed routes as described above. This release label applies to the version served by this host; beta and production are promoted independently.
Release 2026-09-28 · available on this host
Account2 · Retention-aware identities, decoded descriptions and reversal eligibility
Operation ID
Method/path
GetBankAccount
GET /api/v1/banks/{bankKid}/account
ExportBankAccount
GET /api/v1/banks/{bankKid}/account/export
GetBankAccountRevision
GET /api/v1/banks/{bankKid}/account/revision
ReverseBankAccountEntry
POST /api/v1/banks/{bankKid}/account/{transactionKid}/reversal
Previous: listing/export did not apply tenant/manager retention settings. userKid, userName and userNumber could identify expired entries; descriptions could contain partial legacy text or internal payment markers. Ordinary reversal eligibility did not explicitly reject expired or payment-managed consumption.
New: tenant LawAccountingYears and LawSurveillanceDays, with positive manager overrides, mask expired identities in general views: userKid becomes the bank's GDPR user KID (UserId 1000), userName/userNumber become empty strings, description contains only the transaction type, isAnonymized is true and canReverse is false. Zero/missing/invalid retention values retain no identity. With an explicit userKid, expired entries are excluded from rows and totals. CSV/XLSX applies the same rules with unchanged columns. Descriptions use the full shared decoder; internal payment IDs are removed. Payment-managed and expired entries return 422 reversal-unavailable on ordinary reversal. Revision values now include retention visibility and caches are separated by manager. Amount representation and request fields remain unchanged.
Migration: display an anonymous label when isAnonymized is true and never link it to a resident profile. Do not interpret description text as a payment identifier; use the additive paymentKind field (Credit, ReserveRefund, Managed or empty). Honor canReverse and handle 422 without retrying. Do not merge pages with different revisions. The additive documentKey/documentId and documents fields group only filtered lines on each page; merge groups by key across pages, not DocId alone. Existing flat items and row pagination remain supported.
Release: 2026-09-28. The clients and downloadable packages have been synchronized with this contract; package version 0.1.0 is retained and no external registry publication is claimed.
Release 2026-09-28 · available on this host
Icon images · Missing assets are searched in other local sets
Affected responses: an icon missing from the selected set previously returned HTTP 404 even when another local set contained it. Rendering now searches for the same eIcon identity in the selected set first, then other packaged sets in ordinal alphabetical order. Each main/under-icon resolves independently. A matching asset returns HTTP 200 in the requested format, or 304 for a matching conditional request. Unknown sets and icons absent from every local set still return 404. Request fields, format encodings and existing images in the selected set are unchanged. There is no external-server or database fallback. Included in release 2026-09-28.
Operation ID
Method/path
GetIcon2Svg
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2Image
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackground
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GetIconFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Do not interpret a successful render as proof that the asset belongs to the requested set. Use the documentation set catalogs to inspect actual membership. For example, /api/v1/icon/g/house/black/0/0/none.svg now renders the house asset from line. Paths without a set still prefer line. Clients that implement their own 404-based set search can rely on server-side lookup instead. Refresh rendered images or allow the existing ten-minute browser cache to expire; renderer build changes invalidate disk-cache entries.
Endpoints:GET /api/v1/logos/kombine/{color}.svg, GET /api/v1/logos/kombine-text/{color}.svg and GET /api/v1/logos/kombine-logo-text/{color}.svg. The SVG response root previously had width="256" and a proportional numeric height (256, 48.162712 or 51.2). These attributes are now omitted. The unchanged viewBox and preserveAspectRatio="xMidYMid meet" let the complete artwork fit and center in its viewport without cropping or stretching. Routes with an explicit width retain their pixel dimensions. Colors, geometry, content type, operation IDs and status codes are unchanged. Included in release 2026-09-28.
Customer action
If your layout or SVG parser requires fixed dimensions, use the corresponding /{color}/256.svg route, or specify dimensions on the embedding element. Do not assume the unsized response contains numeric width/height attributes. Renderer cache keys have changed; clients can refresh or wait for the existing 600-second browser cache to expire.
Release 2026-09-28 · available on this host
LoginManager · Duplicate credentials now select one active manager
Endpoint:POST /api/v1/session/login. Affected: the status and account selection when multiple managers match both email and password. Previously this case returned HTTP 401. It now returns HTTP 200 for the active, non-deleted manager with the newest eSetting.Alive krumb MS2000, after atomically clearing Password on the other matching managers. Activity uses the krumb timestamp, not Text; missing or invalid timestamps rank last. Equal timestamps, including all unknown, are resolved by the lowest UserId. The selection uses a fresh locked read in the cleanup transaction. Different passwords sharing an email are unchanged. No active match means no cleanup and the lowest matching account determines the existing HTTP 403 account-state error. Cleanup/storage failures or more than 100 matching rows return HTTP 503. Request fields and the token response representation are unchanged. Included in release 2026-09-28.
Customer action
Do not rely on duplicate credentials returning 401. Call GetCurrentManager after login and use its identity and permissions; grants from duplicate accounts are not combined. Sessions of accounts whose passwords are cleared become invalid, subject to the existing maximum 60-second cache on other API instances. To keep distinct accounts usable, give them distinct credentials before this change is released. Do not automatically retry an uncertain login/cleanup response.
Affected requests: remove the literal sets segment after /api/v1/icon/. The previous set-specific paths are removed. The operation IDs, parameter names, rendering, response formats and cache behavior are unchanged. Included in release 2026-09-28.
Operation ID
Previous method/path
New method/path
GetIconFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Remove sets/ from set-specific image URL builders and stored links. For example, use /api/v1/icon/line/house/000000/7/A12/none/128.svg. Keep the set name and all other path values. Existing icon-first routes without a set name still use line; known set names take precedence where route shapes overlap. The rebuilt clients in release 2026-09-28 use these paths.
Release 2026-09-28 · available on this host
GetIcon2Svg / GetIcon2Image / GetIcon2ImageWithBackground · Icon paths and format parameter
Affected requests: the URL prefix for all three GET image operations changes from /Icon2 to /api/v1/icon. The former routes are removed. Operation IDs are retained. The extension parameter is now named format instead of fileType on the sized routes; it is a required path parameter. The short route replaces its fixed .svg suffix with required .{format} and also accepts raster formats, using 128 × 128 pixels when no size is supplied. Existing SVG and sized-image rendering semantics, content types and conditional caching are unchanged. Included in release 2026-09-28.
Operation ID
Previous method/path
New method/path
GetIcon2Svg
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}.svg
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2Image
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}/{size}.{fileType}
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackground
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{fileType}
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Update image URL builders and stored links to use /api/v1/icon/. Use format when binding parameters by their OpenAPI names, and pass svg explicitly for the former SVG-only operation. Keep presentation values in their existing path segments; no query parameters are required. For example, use /api/v1/icon/house/000000/7/A12/none.svg or /api/v1/icon/house/000000/7/A12/none/128.png. Continue URL-encoding individual path values. The rebuilt clients in release 2026-09-28 use these paths.
Release 2026-09-28 · available on this host
GenerateServiceApiKey · Generated keys now start with kt_
Affected: the response field apiKey. Previously, generated keys started with k followed by 64 random ASCII letters and digits (65 characters total). New keys start with kt_ followed by the same 64-character random payload (67 characters total). The payload still includes both uppercase and lowercase letters. Keys remain case-sensitive. The request and all other response fields are unchanged. Included in release 2026-09-28.
Customer action
Allow the underscore and the new total length in fields and validators that consume generated keys. Preserve the exact value returned by the API, including the prefix. Prefer treating keys as opaque strings. Do not add or replace a prefix on existing keys: their stored hashes and login behavior are unchanged. Both existing keys and new kt_ keys remain usable with Equipment service login until replaced or revoked. Client packages were rebuilt for release 2026-09-28.
Release 2026-09-28 · available on this host
SetServiceProfileField · ApiKeyHash no longer accepts manual writes
Affected: the field path parameter and the response to manual hash writes. Previously, ApiKeyHash accepted a caller-supplied hash, or an empty value to clear it, and returned HTTP 200 on success. Only Name and Icon are now accepted. Requests with field=ApiKeyHash return HTTP 400 with problem code invalid-service-profile, regardless of the supplied value. No hash is written or cleared. Included in release 2026-09-28.
Customer action
Remove manual hash editing and clearing. To replace a service key, read its latest profileRevision with GetService, then call GenerateServiceApiKey at POST /api/v1/services/{serviceKid}/api-key:
{"expectedRevision":"<profileRevision from GetService>"}
Use the returned apiKey only after a successful HTTP 200 response and keep details.profileRevision for later edits. Generation replaces the existing key; the plaintext key is returned once. Do not retry automatically after an uncertain response. Manual key import and clearing are no longer supported. Existing stored hashes and response shapes are unchanged. Client packages were rebuilt for release 2026-09-28.
Banks2 location directory
Active location count
GetActiveLocationCount — GET /api/v1/locations/active-count. Returns {"count":123} for the Lokationer/Banks2 icon. Uses the same active-manager, Banks2, Bank Read, Location Read and site/bank/location authorization as GetLocations. Counts only distinct locations with Enabled exactly 1 and Deleted=0, also for all-bank managers. Missing Deleted means zero; missing/invalid Enabled or malformed Deleted are excluded. Limited grants still require a parent bank visible under RetentionDays. Independent of search and paging; banks below 1000 are excluded. The aggregate is read afresh for each request, with permission snapshots at most 60 seconds old. No-store. 401 requires login; 403 means missing tab, read rights or resource access; 503 locations-unavailable means unavailable storage or a 12-second deadline—retry manually and never present it as zero. The portal loads the count in the background after displaying navigation, without delaying the page; it makes one request per navigation element and does not poll. Failed requests leave the badge absent; the icon badge and tooltip show the full count; the red capsule widens to fit the digits.
GetLocations — GET /api/v1/locations lists accessible locations across banks. Requires active manager, Banks2 (5), Bank Read, Location Read and matching site/bank/location grants, rechecked on every page. Location-only grants never expose siblings. With an explicit site-wide grant, all location states are listed, including disabled locations and old deletions; parent-bank deletion does not hide them. With limited grants, only locations whose Enabled value is exactly 1 are listed, and both bank and location deletion follow RetentionDays. Missing Enabled is not enabled. Deleted=0 or missing is visible; positive timestamps must fall within the inclusive window from now minus RetentionDays through now. Zero retention hides all deleted entries; malformed or future deletion values are hidden for limited grants. Reads only site Log24; banks below 1000 are excluded. Discovery requires a Name, Icon, VismaCustNo, Enabled or Deleted location setting.
enabledOnly=true excludes locations whose Enabled value is not exactly 1, before paging. The default is false. This only narrows the authorized results: existing deletion/RetentionDays rules and activation-code permissions still apply. Enabled deleted locations remain visible when authorized. Keep this value on every continuation request; changing it requires restarting without a cursor (otherwise 400 invalid-cursor). Example: GET /api/v1/locations?enabledOnly=true&sort=name&direction=asc&pageSize=50.
items contains kid (location), bankKid, bankName, bankIconKid, name, iconKid, vismaCustNo, bankActivationCode, locationActivationCode, enabled, deleted and deletedAt. Enabled is true only for stored 1. Deleted is false for zero/missing, true for positive MS2000, and null for malformed values. DeletedAt is an ISO 8601 UTC timestamp, or null for zero/invalid/out-of-range values. Use accessible status indicators for disabled, deleted and unknown deletion states. The portal uses one status icon: Enabled=false always means inactive; Enabled=true with Deleted=true means deleted, otherwise active when Deleted=false. The deletion timestamp remains available in the tooltip. These list rules do not change authorization on detail endpoints. KIDs are canonical; no separate numeric identifiers. Missing names/customer numbers are empty strings; invalid icons use bank_building/house. The page-level hasAllBanksAccess flag indicates an explicit site-wide grant, not an enumeration of individual banks. Both codes require this grant; bank codes additionally require Bank Create and location codes require Location Create. Otherwise the code is null. Hide both code columns when the flag is false. These fields never grant API permission.
Optional filter: up to 128 characters, literal case/accent-insensitive substring of bank/location name or VismaCustNo. Exact canonical, readable or site-relative bank/location KIDs are accepted, e.g. 166.2000.4 or 2000.4. Complete activation codes match only if the caller may view the code. Bank codes contain tenant; location codes use the trusted site. No cross-tenant reads.
sort=name (default), bankName or vismaCustNo (external ID, text ordering), direction=asc (default) or desc. SQL utf8mb4_general_ci ordering applies before LIMIT; numeric bank/location IDs break ties in the same direction. pageSize is 1–100, default 50. Send nextCursor unchanged with the same parameters until null. Protected cursors expire after 15 minutes and bind caller, tenant, grants, retention and query. Restart without cursor after changes. Concurrent edits are not a frozen snapshot; renamed rows can move. No total count or full in-memory catalogue.
400: invalid-page, invalid-filter, invalid-sort, invalid-cursor (restart). 401: sign in again. 403: missing-banks-tab, missing-bank-read, missing-location-read, missing-resource-access (correct permissions). 503 locations-unavailable includes the 12-second deadline: show error and allow manual retry. Responses are no-store. Read-only; available in development. Generated clients and downloadable packages are synchronized in version 0.3.2.