Kombine Flex Portal API · v1 · Integration guide

Build your own portal or connect an AI agent

The API provides the same information used by the official portal. Send ordinary HTTPS requests and receive JSON. You do not need database access, a particular SDK, or a special agent protocol.

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 IDMethod and pathPurpose
GetObjectAddressGET /api/v1/addresses/{kid}Read settings and revision
UpdateObjectAddressPUT /api/v1/addresses/{kid}Save Address and Zip, then resolve coordinates
LookupObjectCoordinatesPOST /api/v1/addresses/{kid}/lookupDeliberately resolve the current address again
SetObjectCoordinatesPUT /api/v1/addresses/{kid}/coordinatesSave a manual coordinate pair
SetObjectCoordinateProvenancePUT /api/v1/addresses/{kid}/provenanceChange 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.

curl -H "Authorization: Bearer ACCESS_TOKEN" "$API/api/v1/addresses/$KID"
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "$API/api/v1/addresses/$KID" \
  --data '{"address":"Bjerregade 5","zip":"8722","expectedRevision":"REVISION_FROM_GET"}'
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "$API/api/v1/addresses/$KID/lookup" --data '{"expectedRevision":"LATEST_REVISION"}'
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "$API/api/v1/addresses/$KID/coordinates" \
  --data '{"latitude":55771181,"longitude":9697176,"expectedRevision":"LATEST_REVISION"}'

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.4.1.

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.

curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "$API/api/v1/addresses/$KID/provenance" \
  --data '{"value":30,"expectedRevision":"LATEST_REVISION"}'

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.

curl --fail -o portal.appinstaller https://api.team.kombine.technology/download/windows/x64/portal.appinstaller

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.

curl --get -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://api.team.kombine.technology/api/v1/banks/BANK_KID/documents" \
  --data-urlencode "locationKid=LOCATION_KID" \
  --data-urlencode "from=2026-09-01T00:00:00Z" \
  --data-urlencode "through=2026-09-02T00:00:00Z" \
  --data-urlencode "limit=25" --data-urlencode "offset=0"

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.4.1.

OperationGET pathOutput
GetUnitDocumentTable/api/v1/documents/{documentKid}/tableJSON table and metadata
GetUnitDocumentHtml/api/v1/documents/{documentKid}/table.htmlPrintable HTML table
DownloadUnitDocumentCsv/api/v1/documents/{documentKid}/table.csvUTF-8 CSV
DownloadUnitDocumentXls/api/v1/documents/{documentKid}/table.xlsExcel 97–2003 binary workbook
GetUnitDocumentSvg/api/v1/documents/{documentKid}/graph.svgSVG 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.

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://api.team.kombine.technology/api/v1/documents/DOCUMENT_KID/table.csv?states=Temperature,Level&settings=Cycle" \
  --output document.csv

Limits and errors

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.4.1.

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.

curl -H "Authorization: Bearer $TOKEN" "https://YOUR-PORTAL-API/api/v1/diagnostics/live-logs"

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.

curl -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/hosting/logs?environment=beta&application=portal-api"

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.4.1; 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.

curl --get 'https://beta.api.team.kombine.technology/api/v1/hosting/metrics' \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'environment=beta' \
  --data-urlencode 'application=mysql' \
  --data-urlencode 'hours=6'

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.4.1.

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.

const query = new URLSearchParams({ pageSize: '25' });
if (nextCursor) query.set('cursor', nextCursor);
const response = await fetch(apiBase + '/api/v1/tenant/status/page?' + query, {
  headers: { Authorization: 'Bearer ' + accessToken, 'Accept-Language': 'en-GB' }
});
if ([409, 410].includes(response.status)) { nextCursor = null; /* restart without cursor */ }
else {
  if (!response.ok) throw new Error('HTTP ' + response.status);
  const page = await response.json();
  console.table(page.status.items);
  nextCursor = page.nextCursor;
}

Select Public v1 (no login) in Swagger for anonymous status, statistics and the purchase map. Download its contract as Public OpenAPI JSON. Portal integrations v1 contains manager login and manager operations. This split does not change routes, operation IDs or access control. Clients generating public calls from the previously combined v1 document must now also import the Public document.

Purchases in the last hour

GET /api/v1/public/statistics/purchases requires no login. Its Swagger operation is GetPublicPurchases; the JavaScript client provides client.getPurchases().

Counts rows in the site's A{tenant}.Log1Hour using UserId >= eUserId.Users AND UserId <= eUserId.UsersLast AND Text NOT LIKE '%E' AND Amount < 0. These are purchases, not distinct customers. E matching follows the table's collation. The shared enum defines the inclusive user range, currently 1001–99999. No bank filter is added. The response also includes amount = -SUM(Amount)/100 and currency = MAX(Currency). No purchases returns zero and null. MAX(Currency) assumes a common currency among purchases; no conversion is performed.

The database maintains Log1Hour with cleanup every minute. This operation uses its contents exactly as the agreed SQL does, so the actual window depends on that cleanup. sinceUtc is the nominal start one hour before measuredAtUtc; lookbackHours is 1. tenantKid identifies the site and count is the total. Clients cannot override the tenant or filters.

Results are cached for one minute per API instance. The card refreshes automatically every minute while the page is open. Concurrent requests share one database query. HTTP 503 with Retry-After: 60 means unavailable, not zero. The usual CORS policy applies to JavaScript on other sites.

const response = await fetch('https://api.team.kombine.technology/api/v1/public/statistics/purchases');
if (!response.ok) throw new Error('HTTP ' + response.status);
const sample = await response.json();
console.log(purchases.count, purchases.amount, purchases.currency);

Public data without login

GET /api/v1/public/statistics/active-users — Active-user count for the site's tenant. Swagger: Public → GetPublicActiveUsers.

The count includes distinct bank/user pairs in the tenant's current Log7, with user IDs 1001 through 99999 inclusive, bank ID at least 1000, and MS2000 strictly newer than 100 days before the UTC measurement time. Multiple rows for one pair count once; the same user ID in two banks counts twice. Activity means a recent Log7 record, not necessarily a login. User Enabled and Deleted settings are not additional filters.

This aggregate is public and independent of manager permissions. No individual accounts or per-bank counts are exposed. The site chooses the tenant; this operation accepts no KID, date window, or other filters.

tenantKid identifies the site's tenant, count is the total, lookbackDays is 100, sinceUtc is the exclusive cutoff, and measuredAtUtc is the measurement time. Results are cached for up to 5 minutes per API instance. Concurrent visitors share one query, with no background polling. Database failures return HTTP 503 and Retry-After: 60; display unavailable, not zero. A successful response with count 0 means a genuine zero.

const response = await fetch('https://api.team.kombine.technology/api/v1/public/statistics/active-users');
if (!response.ok) throw new Error('HTTP ' + response.status);
const statistics = await response.json();
console.log(statistics.count, statistics.measuredAtUtc);

The JavaScript client also provides client.getActiveUsers(). For JavaScript on another site, configure the client's exact origin in the API's Cors:AllowedOrigins as described below.

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.

ArtworkPath prefixOperation IDProportions
Symbol/api/v1/logos/kombineGetKombineLogo1:1
Name only/api/v1/logos/kombine-textGetKombineText590:111
Symbol and name/api/v1/logos/kombine-logo-textGetKombineLogoText5: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

Kombine symbol Kombine wordmark White Kombine logo on a dark background

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.4.1.

curl --fail --output kombine.svg "$TENANT_API/api/v1/logos/kombine-logo-text/black/512.svg"

Linear gradients — SVG

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.

VisualOperation IDWith dimensions
Angular gradient backgroundGetCircleGradientGetCircleGradientSized
Percentage circleGetCircleProgressGetCircleProgressSized
Running indicatorGetCircleRunningGetCircleRunningSized
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

Angular color gradient Progress: 65 percent Running indicator

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.4.1.

curl --fail --output progress.svg "$TENANT_API/api/v1/circles/progress/white/22aa88-ffcc33-ee4444/65.svg"

Icon — local icon images

API-computed IconKid

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–1073741823) 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.4.1.

const response = await fetch(`${api}/api/v1/icon/presentation?iconKid=calendar`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { iconKid } = await response.json();
image.src = `${api}/api/v1/icon/g/${encodeURIComponent(iconKid)}.svg`;

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.

curl --fail "$TENANT_API/api/v1/icon/catalog/g"

Open the icon test page.

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 30 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.

C# · Kombine.Flex.Kid

Kombine.Flex.Kid kid = new Kombine.Flex.Kid();
kid.Icons.Add(Kombine.Flex.eIcon.house);
kid.Icons.Add(Kombine.Flex.eIcon.check);
kid.Count = 7;
kid.Color = 0x336699;
kid.Text = "A12";
System.String url = baseUrl + "/api/v1/icon/line/"
    + System.Uri.EscapeDataString(kid.ToString()) + ".svg";

VB.NET · Kombine.Flex.Kid

Dim kid As New Kombine.Flex.Kid()
kid.Icons.Add(Kombine.Flex.eIcon.house)
kid.Icons.Add(Kombine.Flex.eIcon.check)
kid.Count = 7
kid.Color = &H336699
kid.Text = "A12"
Dim url As System.String = baseUrl & "/api/v1/icon/line/" & System.Uri.EscapeDataString(kid.ToString()) & ".svg"
curl --fail --output house.svg "$TENANT_API/api/v1/icon/line/413132x7qE20i11Bi336699Ic.svg"

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.4.1.

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.4.1.

const headers = { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' };
const read = await fetch(`${api}/api/v1/services/${serviceKid}`, { headers });
if (!read.ok) throw new Error(`GetService: ${read.status}`);
const detail = await read.json();
const generated = await fetch(`${api}/api/v1/services/${serviceKid}/api-key`, {
  method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ expectedRevision: detail.profileRevision })
});
if (!generated.ok) throw new Error(`GenerateServiceApiKey: ${generated.status}; reload before retry`);
const confirmed = await generated.json();
const display = document.createElement('code');
display.textContent = confirmed.apiKey;
document.body.append(display);
const copy = document.createElement('button');
copy.textContent = 'Copy API key';
copy.onclick = async () => {
  try { await navigator.clipboard.writeText(display.textContent); }
  catch { copy.textContent = 'Select and copy the key manually'; }
};
document.body.append(copy);
window.addEventListener('pagehide', () => { display.textContent = ''; copy.remove(); }, { once: true });

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.

RequestPurposeAuthentication
GET /api/v1/public/statistics/purchasesPurchase count for the last hourNone
GET /api/v1/public/statistics/active-usersActive-user count for the site's tenantNone
GET /api/v1/statusCheck whether the API responds. Does not check MySQL.None
POST /api/v1/session/loginExchange a manager email and password for a temporary access token.Email and password in JSON
GET /api/v1/session/meRetrieve your own profile and current permissions together.Access token

Try it without writing a program

  1. Open Swagger and select Portal integrations v1. Swagger is a web page for reading about and trying API requests.
  2. Expand GetPortalStatus, select Try it out, then Execute. HTTP 200 means the request succeeded.
  3. 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.
  4. Copy only the accessToken value from the response, without quotation marks. Click Authorize, paste it under ManagerBearer, and authorize. Swagger adds the Bearer prefix.
  5. Run GetCurrentManager. Its response provides the information for your portal’s profile and access cards.

A manager must be enabled and not deleted. Missing Tabs or bank access do not prevent login itself; explain the missing access in your portal.

The HTTP requests

POST https://api.team.kombine.technology/api/v1/session/login
Content-Type: application/json
Accept: application/json

{"email":"[email protected]","password":"<your password>"}

Send the original password over HTTPS. The API verifies it; do not encode or hash it in the client.

{
  "accessToken": "<your access token>",
  "expiresIn": 259200,
  "tokenType": "Bearer"
}
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.

RenewManagerSession

POST https://api.team.kombine.technology/api/v1/session/renew
Authorization: Bearer <current unexpired access token>
Accept: application/json

HTTP 200
{"accessToken":"<replacement token>","expiresIn":259200,"tokenType":"Bearer"}

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.4.1. 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.

$api = 'https://api.team.kombine.technology'
$credential = Get-Credential -Message 'Manager email and password'
$session = $null
$body = $null
$headers = $null
try {
    $body = @{
        email = $credential.UserName
        password = $credential.GetNetworkCredential().Password
    } | ConvertTo-Json -Compress

    $session = Invoke-RestMethod "$api/api/v1/session/login" `
        -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 15

    $headers = @{ Authorization = "Bearer $($session.accessToken)" }
    $profile = Invoke-RestMethod "$api/api/v1/session/me" `
        -Headers $headers -TimeoutSec 15
    $profile | ConvertTo-Json -Depth 6
}
catch {
    if ($_.Exception.Response) {
        Write-Warning "HTTP $([int]$_.Exception.Response.StatusCode). See the error table."
    } else {
        Write-Warning 'Could not reach the API. Check the address, connection, and certificate.'
    }
}
finally {
    $body = $null
    $headers = $null
    $session = $null
    $credential = $null
}

Personal manager settings

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 IDHTTP
GetMyManagerProfileGET /api/v1/session/me/profile
SetMyManagerProfileFieldPOST /api/v1/session/me/profile/{field}
GetMyManagerTabsGET /api/v1/session/me/tabs
SetMyManagerTabPOST /api/v1/session/me/tabs/{tabId}
RequestMyManagerEmailVerificationPOST /api/v1/session/me/email-verification
ConfirmMyManagerEmailPOST /api/v1/session/me/email-confirmation
ChangeMyManagerPasswordPOST /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.

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://api.team.kombine.technology/api/v1/session/me/profile"

curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "https://api.team.kombine.technology/api/v1/session/me/profile/Name" \
  -d '{"revision":"REVISION_FROM_PROFILE","value":"Alex Jensen"}'

curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
  "https://api.team.kombine.technology/api/v1/session/me/profile/RetentionDays" \
  -d '{"revision":"REVISION_FROM_LAST_RESPONSE","value":30}'

Choose your own tabs

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.4.1. 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.4.1.

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.4.1.

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.4.10

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.

{
  "kid": "3E7Q46o3B9ACA01h",
  "retentionDays": 30,
  "themeMode": 0,
  "databaseAccess": {"canWrite": false, "checkedAtUtc": "2026-09-25T12:00:00Z"},
  "organisation": "Example organisation",
  "name": "Example manager",
  "iconKid": "house",
  "tabs": [4, 12],
  "hasBankAccess": true,
  "tabDetails": [{"id": 4, "name": "Bank1"}, {"id": 12, "name": "Dashboard1"}],
  "resourceGrants": [{"kid": "3E7Q14o2Ab", "scope": "Bank"}],
  "navigationBanks": [{"kid": "3E7Q14o2Ab", "name": "Example bank", "iconKid": "house"}],
  "operationPermissions": [
    {"resource": "Managers", "level": "Read", "flags": 1, "canRead": true, "canWrite": false, "canCreate": false, "canDelete": false, "canRenameExternalId": false, "canRename": false},
    {"resource": "Bank", "level": "Read", "flags": 1, "canRead": true, "canWrite": false, "canCreate": false},
    {"resource": "Location", "level": "3", "flags": 3, "canRead": true, "canWrite": true, "canCreate": false},
    {"resource": "Unit", "level": "7", "flags": 7, "canRead": true, "canWrite": true, "canCreate": true},
    {"resource": "User", "level": null, "flags": null, "canRead": false, "canWrite": false, "canCreate": false},
    {"resource": "Installer", "level": "Read", "flags": 1, "canRead": true, "canWrite": false, "canCreate": false, "canDelete": false, "canRenameExternalId": false, "canRename": false},
    {"resource": "Service", "level": "Read", "flags": 1, "canRead": true, "canWrite": false, "canCreate": false, "canDelete": false, "canRenameExternalId": false, "canRename": false}
  ]
}
FieldMeaning and use
organisationOptional 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.
retentionDaysDays 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.
kidThe manager ID as a string. Store and pass it unchanged. me returns only the signed-in manager. An ID does not grant access.
name, iconKidDisplay 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, tabDetailsPermitted 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.
hasBankAccessWhether at least one bank or location grant applies on this site. This is not blanket access to data.
resourceGrantsThe specific areas the manager may access, explained below. Does not contain bank names or actual bank records.
operationPermissionsFour 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.

const access = profile.databaseAccess;
const accessText = access?.canWrite === true ? 'Database: Read and write access'
  : access?.canWrite === false ? 'Database: Read-only access'
  : 'Database access: Unknown';

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 / codeMeaningClient action
400Invalid JSON, email, or missing fields.Correct the input. Email is limited to 254 characters; password to 1,024.
401 on loginCredentials do not match a valid manager.Show “Incorrect email or password.” Do not retry automatically.
401 on meMissing, 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 / disabledThe manager is disabled.Explain that the manager is disabled and suggest contacting the administrator.
403 / deletedThe manager is deleted.Explain that the manager is deleted and suggest contacting the administrator.
403 / account-settingsEnabled is missing or invalid, or a present Deleted value is invalid.Explain that account setup is incomplete and needs administrator attention.
413 / 415Oversized login body / unsupported content type.Send only email and password as application/json. Login body limit: 8,192 bytes.
429Too many login attempts.Wait at least Retry-After seconds (currently 60). Default to 60 seconds if absent.
503Required data cannot be loaded.Display a temporary service error. Offer a retry after a pause.

Login reveals a 403 reason only after verifying the password. Translate the codes in your own interface. Do not depend on English error titles or a particular traceId.

4. Your own portal

Complete JavaScript example

Open the working JavaScript example. It includes API address, email, and password fields and buttons for status, login, profile refresh, and logout. It uses ordinary fetch without npm packages or a framework.

To use it in your portal, copy portal-api.mjs into your JavaScript folder. It is a small optional example; direct fetch calls also work, as shown below. Create one client per user and API site:

import { createPortalClient } from './portal-api.mjs';

const portal = createPortalClient('https://api.team.kombine.technology');
// Obtain email and password from your login form.
await portal.login(email, password);
const manager = await portal.getProfile();
// Display manager.name, manager.tabDetails, and manager.operationPermissions.
// Call portal.getProfile() for manual refresh and portal.logout() for logout.

Load your own script with <script type="module" src="./app.mjs"></script>. Host the example files on your own web server; do not open them using file:// or import the module directly from another API origin. The same module works in Node.js with built-in fetch. Server-side JavaScript does not need CORS.

The client keeps only the token in memory, reuses it, clears it after HTTP 401, and provides errors with status, code, and retryAfter for HTTP 429. Network, certificate, timeout, and CORS failures may have no HTTP status. There are no automatic retries or polling. Do not share a client instance between users on a Node.js server.

A portal with its own server

Have your server call the API and keep the access token in each user’s protected session. This is also how the official Blazor portal works. Never share one manager session between users. The browser uses your portal’s own cookie/login while your server sends the bearer token to the API. This does not require CORS.

A browser calling the API directly

This is also supported. When your portal uses a different origin from the API, the API administrator must add its exact origin to Cors:AllowedOrigins. An origin is the scheme, hostname, and optional port, without a path or trailing slash. The default list is empty.

{
  "Cors": {
    "AllowedOrigins": ["https://my-portal.example", "https://localhost:5173"]
  }
}

This is server configuration and requires a restart. The environment variable for the first origin is Cors__AllowedOrigins__0. Only listed origins can read integration responses through a browser. Authentication and permissions still apply. CORS is a browser rule, not access control for server programs.

If your development site runs at http://localhost:5173, add exactly that address; https://localhost:5173 is a different origin. Use the API’s HTTPS address. The browser automatically sends OPTIONS before applicable requests; the API handles it. Do not use mode: 'no-cors': your code would be unable to read the JSON response.

Call this small browser function with the values from your login form. It returns the profile without persisting the token in localStorage or placing it in the URL.

async function loginAndLoadProfile(apiBaseUrl, email, password) {
  const base = apiBaseUrl.replace(/\/$/, '');
  const login = await fetch(`${base}/api/v1/session/login`, {
    method: 'POST',
    credentials: 'omit',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({ email, password }),
    signal: AbortSignal.timeout(15000)
  });
  if (!login.ok) {
    const error = await login.json().catch(() => ({}));
    throw new Error(`Login: HTTP ${login.status}, ${error.code ?? ''}`);
  }
  const session = await login.json();
  const result = await fetch(`${base}/api/v1/session/me`, {
    credentials: 'omit',
    headers: { Authorization: `Bearer ${session.accessToken}`, Accept: 'application/json' },
    signal: AbortSignal.timeout(15000)
  });
  if (!result.ok) throw new Error(`Profile: HTTP ${result.status}`);
  return await result.json();
}

This example demonstrates the first exchange. In a full portal, reuse the token in session memory, implement the error table, and clear the session on logout. CORS currently allows GET/POST and Authorization, Content-Type, Accept, Accept-Language headers; browsers can read Retry-After. Cookies are not shared with the API.

Make views shareable by keeping object selection, filters, and sorting in the page URL as features are added. Recipients log in as themselves. Never include tokens, passwords, or sensitive record contents in URLs. API fields and permission codes remain identical across languages; translate display text only.

5. AI agents use the same API

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Each link also includes optional name, canonical parent bankKid and API-produced iconKid. For example, render name ?? kid as the link label and use path as its destination. The official portal adds the clicked bank/location to the local workspace and opens it; navigation rechecks access. Links come only from successful current API reads, including location references in status results, never from generated text.

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.4.1.

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.

const page = await client.getBankUsers(bankKid, {
  pageSize: 25, sort: 'name', direction: 'asc'
});
const next = page.nextCursor
  ? await client.getBankUsers(bankKid, {
      pageSize: 25, sort: 'name', direction: 'asc', cursor: page.nextCursor
    })
  : null;

Keep sort, direction and pageSize during traversal. When changing order, restart without a cursor. A cursor for another sort or direction returns 400. Sorted cursors are positions in the current authorized list; data or permission changes can shift positions between requests. Shared links never grant the sender's permissions.

Sorting uses a shared four-setting index cached for up to 60 seconds across managers and sort choices. The API filters permissions and sorts in memory; only the selected page fetches the remaining details. There is no COUNT, SQL OFFSET or query per user. A cold cache scans the bank's ordinary users; very large banks may hit the timeout and return 503. Avoid immediate automatic retries. Identity mode retains its bounded 1,000-candidate scan and scanLimitReached behavior.

The official portal uses GetBankUsers for progressive loading while scrolling. External portals can do the same: request one nextCursor at a time, reuse the result and stop at null. Stop automatic loading on errors and offer an explicit retry. Every request is authorized, including cursors received in a shared link from a colleague.

Users in a bank (Users2)

GET /api/v1/banks/{bankKid}/users?pageSize=25 · operation GetBankUsers. Take the bank KID from navigationBanks or a Bank scope in resourceGrants. Send the manager bearer token. No separate tenant, bank or user IDs are accepted.

const page = await client.getBankUsers(bankKid, { pageSize: 25 });
for (const user of page.items) console.log(user.kid, user.name, user.number);
if (page.nextCursor) {
  const next = await client.getBankUsers(bankKid, {
    pageSize: 25, cursor: page.nextCursor
  });
}

The response contains items, previousCursor, nextCursor and scanLimitReached. Each user has kid, name (99), number (1824), 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.

{"email":"[email protected]","sms":"+1 202-555-0123"}

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.

const response = await fetch(`${api}/api/v1/banks/${encodeURIComponent(bankKid)}/locations`, {
  headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Location request failed: ${response.status}`);
const locations = await response.json();

Requires an active manager, at least one assigned Tab, Location Read (also included in Write/Create), and matching site/bank/location access. Location-only grants return only those locations. 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.

const response = await fetch(`${api}/api/v1/locations/${encodeURIComponent(locationKid)}/units`, {
  headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Unit request failed: ${response.status}`);
const { location, items } = await response.json();

Requires an active manager, at least one Tab, Location Read and Unit Read (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.4.1 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.4.1 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.

curl -G "$API_BASE/api/v1/banks/icons" -H "Authorization: Bearer $TOKEN" --data-urlencode "kid=$BANK_KID"
{"items":[{"kid":"…","iconKid":"…","offline":true,"status":200}]}

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.

const response = await fetch(`${api}/api/v1/units/${encodeURIComponent(unitKid)}`, {
  headers: { Authorization: `Bearer ${token}`, "Accept-Language": "en-GB" }
});
if (!response.ok) throw new Error(`Unit request failed: ${response.status}`);
const { unit, descriptorAvailable, settingGroups, stateGroups } = await response.json();
  • 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.

curl "$API/api/v1/units/$UNIT_KID/groups/settings/Core/Name/history?limit=25" \
  -H "Authorization: Bearer $TOKEN"

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.

const response = await fetch(`${api}/api/v1/locations/${locationKid}/units`, { headers: { Authorization: `Bearer ${token}`, "Accept-Language": "en-GB" } });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const overview = await response.json();

Current eCycle

GetLocationUnits now returns cycle (stable eCycle name) and cycleText (Accept-Language) per unit. Both are null for missing/invalid values. The newest MS2000 from eSetting.Cycle (1619, Settings) and eState.Cycle (20, States) wins; States wins ties. An invalid newest value never falls back to an older value. Unit data is now cached for at most 10 seconds; location labels and manager permissions for up to one minute. Both SQL branches are bank/location scoped. The portal refreshes every 10 seconds, pauses in hidden tabs, prevents overlapping requests and hides stale details on failure. External clients can repeat this GET with the same bearer token and Accept-Language every 10 seconds.

Settlement: viewing, history and downloads

Every call requires Authorization: Bearer TOKEN, the Settlement2 Tab, Bank Read, User Read and access to the entire bank. Location-only access cannot expose bank-wide settlement data. Permissions are checked on every request with a manager snapshot cached for at most one minute. These GET operations never close, undo or modify a settlement.

OperationGET path
GetBankSettlements/api/v1/banks/{bankKid}/settlements?beforePeriod=123
GetBankSettlementPeriod/api/v1/banks/{bankKid}/settlements/{period}
DownloadBankSettlement/api/v1/banks/{bankKid}/settlements/{period}/download?format=XLS

Omit beforePeriod initially. History returns up to 25 closed periods, nextSettlement and nextBeforePeriod. Pass the next cursor unchanged; null means the end. Unknown dates and values are null. Dates use UTC. Period 0 is the provisional current period, available through details and downloads; it can change until settlement.

Details contain sourceEntries, includedEntries, groups and formats. Each group has group, currency, entries and amountMinor. Amounts are signed database values in minor units, not formatted currency amounts. Different currencies are never added together. Stored history totals may differ from export totals.

ChargePoint_58/TimeNew power consumption is excluded; cash banks also exclude Month and Transfer. Group priority is ETest, EInstaller, EGuest, configured number masks (U), EDate, then LR. Masks use % for multiple characters and _ for one character. Valid legacy entries classified as Unknown retain their recorded amounts. Current user numbers, names, tags and attributes are used, so regenerating a historical file may differ from its original version. Settlement is exempt from RetentionDays: deleted users remain included in period details and downloads regardless of deletion age. Normal manager, Tab, bank and read permissions are still enforced.

The ZIP contains one file per group and currency plus a reconciliation manifest.json. Empty periods contain only the manifest. Text formats may omit groups with no exportable amounts; group totals remain in the manifest. XLS produces real .xlsx workbooks with Number, Amount and UserId following the export convention; UserId in this compatibility file is numeric, while HTTP object identifiers are KIDs. Other formats use UTF-8 text with CRLF. Excel keeps the database amount sign; NAVISION, for example, reverses it.

Formats: XLS, ATB, BL, DEAS, FRUEHØJGAARD, HEIMSTADEN, LEJERBO, MD90_1, MD90_3, MD90_3_minus, MD90_3_plus, MD90_3_AABKBH and NAVISION. MD90_3 is defined for banks 1001 and 1068 only. NIRAS and ROBERT are obsolete. HUMAN, KMD, LYKKEBO and MD90 are not offered because the reviewed shared code contains no implemented export for them. Format identifiers are case-sensitive; use the list returned in the details.

const headers = { Authorization: `Bearer ${token}` };
const base = `/api/v1/banks/${encodeURIComponent(bankKid)}/settlements`;
const details = await fetch(`${base}/12`, { headers });
if (!details.ok) throw new Error(`HTTP ${details.status}`);
const period = await details.json();
const download = await fetch(`${base}/12/download?format=XLS`, { headers });
if (!download.ok) throw new Error(`HTTP ${download.status}`);
const url = URL.createObjectURL(await download.blob());
const link = document.createElement('a');
link.href = url; link.download = 'settlement-12.zip'; link.click();
setTimeout(() => URL.revokeObjectURL(url), 60000);

This example assumes the same origin. External browser portals use the full API base address and an allowed CORS origin. 400 means invalid KID/period/format, 401 requires login, 403 means denied permissions, 404 means an unknown closed period, 422 means data cannot be exported safely, and 503 means temporarily unavailable data. Causes of 422 include invalid transaction codes, invalid numbers/currency, duplicate export numbers across users, field width overflow or more than 100,000 entries/10,000 users. No partial files are returned. Correct data/format instead of retrying 422. Back off before retrying 503.

History and period sources are cached for at most one minute, coalescing concurrent reads. Details load only when a period is selected. No background database reads or MySQL writes occur. Legacy readiness calculations and automatic jobs have not been migrated; this API does not claim a bank is ready to close.

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.

qrCodeDataV1 is the complete QR payload, not an image. It preserves FlexORM's GetQRCodeString() format: the tenant URL, #, and a FlexCipherLongs fragment containing the bank code, user code and UTC seconds since 2000-01-01. Render the string unchanged with your own QR library (including a quiet zone); never send activation credentials to an external QR service. An empty string means the tenant has no activation URL. Reload the endpoint for a fresh timestamp; validity is determined by the existing activation consumer. Requires an active manager, Users2, User Read, User Create and a bank-wide grant. Invalid KIDs return 400, invalid sessions 401, insufficient permissions 403, missing/deleted residents 404 and unavailable storage 503. The response is not cacheable.

QR version 2: qrCodeDataV2 encodes five FlexCipherLongs values in order: bankCode, userCode, seconds, noise, checksum. The first three match version 1 in the same response. Noise is freshly generated using a cryptographic random generator for each payload, uniformly from 0–1073741823 (30 bits); repeats are possible. Checksum is (((bankCode * 31 + userCode) * 31 + seconds) * 31 + noise) % 1073741824. For overflow-safe calculation, start with c = bankCode % M, then apply c = (c * 31 + value % M) % M for userCode, seconds and noise, with M = 1073741824 and UInt64 intermediates. Decode using FlexCipherLongs.Parse(5, fragment), validate the parse, check both noise and checksum are at most 1073741823, and verify the fifth value against the calculation. These are logical 30-bit unsigned values encoded as numbers, not fixed-width byte fields. Noise does not authenticate the payload or prevent replay; checksum is error detection only. Migrate previous unreleased four-value version 2 readers and regenerate their QR codes; no fallback to older version 2 formats. Version 1 remains three values. Both fields are empty if the tenant has no activation URL. Render the payload locally without modification.

curl --fail-with-body -H "Authorization: Bearer $TOKEN" "$API/api/v1/banks/$BANK_KID/users/$USER_KID/activation"
# Render response.qrCodeDataV1 locally as a QR code; do not log the response.

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.

const page = await fetch(`${base}/api/v1/banks/${encodeURIComponent(bankKid)}/bookings?from=2026-09-01&through=2026-09-30&status=all&limit=50`, {
  headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Language': 'en-GB' }
}).then(async response => {
  if (!response.ok) throw await response.json();
  return response.json();
});

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.4.1.

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.

const filters = new URLSearchParams({from: '2026-09-01', through: '2026-09-30',
  timeZone: 'Europe/Copenhagen', kind: 'all', limit: '50'});
const accountUrl = `${base}/api/v1/banks/${encodeURIComponent(bankKid)}/account`;
const response = await fetch(`${accountUrl}?${filters}`, {
  headers: {Authorization: `Bearer ${accessToken}`, 'Accept-Language': 'en-GB'}
});
if (!response.ok) throw new Error(`Account HTTP ${response.status}`);
const page = await response.json();
for (const row of page.items) console.log(row.kid, row.recordedAtUtc, row.amountMinor, row.currency);

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.

const exportResponse = await fetch(`${accountUrl}/export?${filters}&format=xlsx`, {
  headers: {Authorization: `Bearer ${accessToken}`}
});
if (!exportResponse.ok) throw new Error(`Export HTTP ${exportResponse.status}`);
const workbook = await exportResponse.blob();

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.

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.

fetch(`${api}/api/v1/search/users?q=${encodeURIComponent("[email protected]")}`, { headers: { Authorization: `Bearer ${token}` } })

Parent banks in location results

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.

fetch(`${api}/api/v1/search/user-sms?q=51573605`, { headers: { Authorization: `Bearer ${token}` } })

Consumption: missing read permission

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.

{"status":403,"code":"forbidden","reason":"missing-unit-read"}

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.

{"status":403,"code":"forbidden","reason":"missing-unit-read"}

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.

{
  "kid": "<resident KID returned by GetBankUsers>",
  "status": "ok",
  "currentBalanceMinor": -12345,
  "previousBalanceMinor": -25000,
  "previousPeriod": 23,
  "previousPeriodIsProvisional": false,
  "balances": [
    { "currency": "DKK", "currentBalanceMinor": -10000, "previousBalanceMinor": -20000, "previousPeriod": 23, "previousPeriodIsProvisional": false },
    { "currency": "EUR", "currentBalanceMinor": -2345, "previousBalanceMinor": -5000, "previousPeriod": 23, "previousPeriodIsProvisional": false }
  ]
}

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.4.1 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.

curl -fsS "$API/api/v1/installers?pageSize=50&sort=lastActive&direction=desc" \
  -H "Authorization: Bearer $TOKEN"

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, PermissionUser2 PermissionInstaller2 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.

{"retentionDays":30,"operationPermissions":[{"resource":"User","level":"RenameExtrenatId, Rename","flags":48,"canRead":false,"canWrite":false,"canCreate":false,"canDelete":false,"canRenameExternalId":true,"canRename":true}]}

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.

MANAGER_KID='insert kid from GetManagers'
curl "$API_BASE/api/v1/managers/$MANAGER_KID" \
  -H "Authorization: Bearer $TOKEN"

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.

curl --get "$API_BASE/api/v1/managers" \
  --data-urlencode "[email protected]" \
  --data-urlencode "pageSize=50" \
  --data-urlencode "sort=name" --data-urlencode "direction=asc" \
  -H "Authorization: Bearer $TOKEN"

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.

curl "$API_BASE/api/v1/managers?pageSize=50" \
  -H "Authorization: Bearer $TOKEN"
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.

curl --get "$API_BASE/api/v1/managers" \
  --data-urlencode "sort=lastActive" --data-urlencode "direction=desc" \
  --data-urlencode "pageSize=50" -H "Authorization: Bearer $TOKEN"

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.

curl -X POST "$API_BASE/api/v1/managers/$MANAGER_KID/permissions/Bank" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data '{"flag":2,"enabled":true,"expectedFlags":1}'

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.

roleBankLocation, Unit, UserManagers, 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:

curl -X POST "$API_BASE/api/v1/managers/$MANAGER_KID/permission-role" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data "{\"role\":\"accounting\",\"expectedTabsRevision\":\"$TABS_REVISION\",\"expectedFlags\":{\"Managers\":1,\"Installer\":1,\"Service\":1,\"Bank\":1,\"Location\":1,\"Unit\":1,\"User\":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.
curl "$API/api/v1/managers/$MANAGER_KID/kids" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @- <<JSON
{"resourceKid":"$RESOURCE_KID","enabled":true,"expectedRevision":"$KIDS_REVISION"}
JSON

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:

fieldJSON valueStored meaning
Name / OrganisationString, up to 200 characters, no controlsUnicode/whitespace preserved; empty clears the field.
Enabledtrue / false1 / 0
Deletedtrue / falseServer deletion time in MS2000 / 0
RetentionDaysInteger 0–2147483647Days of visibility for otherwise authorized deleted records.
IconExact eIcon name as a stringOnly icons with eIconSubject.Person metadata may be newly assigned.
EmailOne nonempty email address, up to 254 charactersLogin 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.

PROFILE_REVISION='copy latest profileRevision from GetManager'
curl "$API_BASE/api/v1/managers/$MANAGER_KID/profile/Email" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @- <<JSON
{"value":"[email protected]","expectedRevision":"$PROFILE_REVISION"}
JSON

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.

PROFILE_REVISION='copy latest profileRevision from GetManager'
curl "$API_BASE/api/v1/managers/$MANAGER_KID/profile/Icon" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @- <<JSON
{"value":"angel","expectedRevision":"$PROFILE_REVISION"}
JSON
PROFILE_REVISION='copy profileRevision from GetManager'
curl -X POST "$API_BASE/api/v1/managers/$MANAGER_KID/profile/Name" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data "{\"value\":\"New display name\",\"expectedRevision\":\"$PROFILE_REVISION\"}"

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.

Client source, tests and generators are publicly available on GitHub.

API contract changelog · Review request/response changes before updating your integration.

.NET client without Kombine dependencies

Download NuGet package (.nupkg) · 0.4.1 NuGet.org NuGet.org: production releases

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 platformClient asset selected by NuGetDependencies
.NET Framework 4.7.2 / 4.8 / 4.8.1.NET Standard 2.0Microsoft System.Text.Json 10.0.12 and its Microsoft dependencies
.NET 8 / 9.NET 8No additional packages
.NET 10.NET 10No 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.

dotnet add package Kombine.Flex.Portal.Client --version 0.4.1 --source https://api.nuget.org/v3/index.json

Put the supplied Kombine.Flex.Portal.Client.0.4.1.nupkg in a packages directory next to your project, then install from that local directory:

dotnet nuget add source ./packages --name flex-local
dotnet add package Kombine.Flex.Portal.Client --version 0.4.1

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.

python -m pip install ./kombine_flex_portal_client-0.4.1-py3-none-any.whl

Login and tabs

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.

page = api.get_bank_users(bank_kid, page_size=25, sort="number", direction="asc")
balances = api.get_bank_user_balances(bank_kid, {"userKids": user_kids})

Downloads, errors and timeouts

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.

JavaScript / TypeScript

Node.js 22+ / modern browsers · npm / ESM · 100 API-operations

@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.

npm install ./kombine-flex-portal-client-0.4.1.tgz

Login and tabs

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.

const page = await api.getBankUsers(bankKid, { pageSize: 25, sort: 'number', direction: 'asc' });
const balances = await api.getBankUserBalances(bankKid, { userKids });

Downloads, errors and timeouts

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.

See CORS configuration and the small JavaScript demo. The demo’s portal-api.mjs is a separate, smaller example with its own method surface.

API contract changelog · Review request/response changes before updating your integration.

.NET Framework 2.0 and Visual Studio 2008 without NuGet

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.

.NET Framework 4.5 / Visual Studio 2012

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.4.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.

Download PHP ZIP · PHP checksums · API contract changelog

Save the ZIP in your application’s packages/ folder, then install through Composer (ext-zip is needed during installation):

composer config repositories.kombine artifact ./packages
composer require kombine/flex-portal-client:0.4.2

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-05 · available on this host · clients 0.4.1, PHP 0.4.2

Location directory optional fields must be requested

GetLocations — GET /api/v1/locations: previously vismaCustNo was always a string and both activation-code fields were populated whenever authorized. They now default to null unless explicitly selected with the new comma-separated fields query parameter. Select vismaCustNo,bankActivationCode,locationActivationCode to retain the previous values (code permissions still apply). New optional fields are address, zip, longitude and latitude. Status, canonical KIDs, names and icons remain present. The page adds a normalized fields array.

Migration: request every optional field your client consumes and accept null for unselected values. Send the same selection on every page; restart without the old cursor after changing it. Unknown fields return 400 invalid-fields, and changing a cursor's selection returns 400 invalid-cursor. Packaged clients 0.4.1 include this contract.

Release 2026-10-05 · available on this host · clients 0.4.1, PHP 0.4.2

Version 2 QR noise and checksum expanded to 30 bits

GetBankUserActivation — GET /api/v1/banks/{bankKid}/users/{userKid}/activation: in qrCodeDataV2, noise and checksum (the fourth and fifth decoded values) change from unsigned 24-bit values (0–16777215) to unsigned 30-bit values (0–1073741823). The checksum modulus changes from 16777216 to 1073741824: (((bankCode * 31 + userCode) * 31 + seconds) * 31 + noise) % 1073741824. Noise is freshly generated with a cryptographic random generator. The five-value order and JSON string representation are unchanged; version 1 is unchanged.

Migration: continue decoding five values, expand range validation for both noise and checksum to 30 bits, and use the new modulus at each checksum step with UInt64 intermediates. Regenerate QR codes from the earlier unreleased version 2 format; no old-modulus fallback. Packaged clients 0.4.1 and PHP 0.4.2 include this contract.

Development revision · superseded before beta release

Version 2 QR payload adds random noise before the checksum

GetBankUserActivation — GET /api/v1/banks/{bankKid}/users/{userKid}/activation: qrCodeDataV2 changes from four encoded numbers (bankCode, userCode, seconds, checksum) to five (bankCode, userCode, seconds, noise, checksum). Noise is a fresh cryptographically generated unsigned 24-bit value, 0–16777215. The checksum remains 24-bit but now includes noise: (((bankCode * 31 + userCode) * 31 + seconds) * 31 + noise) % 16777216. The JSON field remains a non-null string; version 1 is unchanged.

Migration: decode five values with FlexCipherLongs.Parse(5, fragment), validate the fourth and fifth values as 24-bit, and include noise when verifying the fifth value. Reduce after each arithmetic step using UInt64 intermediates. Regenerate earlier unreleased version 2 QR codes; no compatibility fallback. Noise may repeat and is not authentication or replay protection. Packaged clients will be synchronized at beta preparation.

Development revision · superseded before beta release

Version 2 QR checksum changed to weighted 24-bit arithmetic

GetBankUserActivation — GET /api/v1/banks/{bankKid}/users/{userKid}/activation: the fourth decoded value in qrCodeDataV2 previously used the unsigned 16-bit sum (bankCode + userCode + seconds) % 65536. It now uses the unsigned 24-bit value ((bankCode * 31 + userCode) * 31 + seconds) % 16777216, in the range 0–16777215. The JSON field remains a non-null string, and FlexCipherLongs still encodes four numeric values. This is a logical three-byte checksum, not a separate fixed-width byte field. Version 1 and permission requirements are unchanged.

Migration: update version 2 readers to validate the new range and formula. Reduce each input modulo 16777216 before multiplication/addition and use wide intermediate integers to avoid overflow. Regenerate QR codes from the earlier unreleased version 2 format; the old checksum is not accepted as a fallback. This checksum is error detection, not authentication. Packaged clients will be synchronized during beta release preparation.

Release 2026-10-05 · available on this host · clients 0.4.1, PHP 0.4.2

Explicit version 1 name for resident QR data

GetBankUserActivation — GET /api/v1/banks/{bankKid}/users/{userKid}/activation: the response field qrCodeData is renamed to qrCodeDataV1. The old field is removed without an alias. The value remains a non-null JSON string with the unchanged version 1 FlexCipherLongs payload, or an empty string when no tenant activation URL exists. qrCodeDataV2 and authorization requirements are unchanged.

Migration: rename the response property in your model and read qrCodeDataV1 when rendering version 1. No QR payload or decoding changes are needed. Packaged clients will be synchronized during beta release preparation.

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 IDUnchanged method/path
GetCurrentManagerGET /api/v1/session/me
GetMyManagerProfileGET /api/v1/session/me/profile
SetMyManagerProfileFieldPOST /api/v1/session/me/profile/{field}
GetManagersGET /api/v1/managers
GetManagerGET /api/v1/managers/{managerKid}
SetManagerProfileFieldPOST /api/v1/managers/{managerKid}/profile/{field}
SetManagerTabPOST /api/v1/managers/{managerKid}/tabs/{tabId}
SetManagerPermissionRolePOST /api/v1/managers/{managerKid}/permission-role
GetInstallersGET /api/v1/installers
GetInstallerGET /api/v1/installers/{installerKid}
SetInstallerIconPOST /api/v1/installers/{installerKid}/icon
GetServicesGET /api/v1/services
GetServiceGET /api/v1/services/{serviceKid}
SetServiceProfileFieldPOST /api/v1/services/{serviceKid}/profile/{field}
GenerateServiceApiKeyPOST /api/v1/services/{serviceKid}/api-key
GetLocationsGET /api/v1/locations
GetBankLocationsGET /api/v1/banks/{bankKid}/locations
GetLocationUnitsGET /api/v1/locations/{locationKid}/units
GetUnitOverviewGET /api/v1/units/{unitKid}
GetUnitGroupGET /api/v1/units/{unitKid}/groups/{kind}/{group}
SetUnitSettingPOST /api/v1/units/{unitKid}/groups/settings/{group}/{setting}
GetUnitSettingHistoryGET /api/v1/units/{unitKid}/groups/settings/{group}/{setting}/history
GetBankUsersGET /api/v1/banks/{bankKid}/users
GetBankUserWorkspaceGET /api/v1/banks/{bankKid}/users/{userKid}/workspace
CreateBankUserPOST /api/v1/banks/{bankKid}/users
ExecuteBankUserCommandPOST /api/v1/banks/{bankKid}/users/{userKid}/commands
GetBankBookingsGET /api/v1/banks/{bankKid}/bookings
ExecuteBankBookingCommandPOST /api/v1/banks/{bankKid}/bookings/{bookingKid}/commands
GetBankAccountGET /api/v1/banks/{bankKid}/account
GetBankDocumentsGET /api/v1/banks/{bankKid}/documents
GetUnitDocumentTableGET /api/v1/documents/{documentKid}/table
GetTenantStatusGET /api/v1/tenant/status
SearchBanksGET /api/v1/search/banks
SearchBankActivationGET /api/v1/search/bank-activation
GetSearchBankGET /api/v1/search/banks/{bankKid}
SearchLocationsGET /api/v1/search/locations
SearchLocationActivationGET /api/v1/search/location-activation
SearchUsersGET /api/v1/search/users
SearchUserSmsGET /api/v1/search/user-sms
SearchUserActivationGET /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 IDPrevious method/pathNew method/path
GetIconFromSetGET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}GET /api/v1/icon/{iconSet}/{kid}.{format}
GetIconImageFromSetGET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}GET /api/v1/icon/{iconSet}/{kid}/{size}.{format}
GetIconImageWithBackgroundFromSetGET /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 IDMethod/path
GetBankAccountGET /api/v1/banks/{bankKid}/account
ExportBankAccountGET /api/v1/banks/{bankKid}/account/export
GetBankAccountRevisionGET /api/v1/banks/{bankKid}/account/revision
ReverseBankAccountEntryPOST /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 IDMethod/path
GetIcon2SvgGET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2ImageGET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackgroundGET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GetIconFromSetGET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSetGET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSetGET /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.

Release 2026-09-28 · available on this host

GetKombineLogo / GetKombineText / GetKombineLogoText · Responsive SVG dimensions

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.

Release 2026-09-28 · available on this host

GetIconFromSet / GetIconImageFromSet / GetIconImageWithBackgroundFromSet · Shorter icon-set paths

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 IDPrevious method/pathNew method/path
GetIconFromSetGET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSetGET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSetGET /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 IDPrevious method/pathNew method/path
GetIcon2SvgGET /Icon2/{kid}/{color}/{count}/{text}/{sub}.svgGET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2ImageGET /Icon2/{kid}/{color}/{count}/{text}/{sub}/{size}.{fileType}GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackgroundGET /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_

Endpoint: POST /api/v1/services/{serviceKid}/api-key

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

Endpoint: POST /api/v1/services/{serviceKid}/profile/{field}

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.

curl "$BASE/api/v1/locations/active-count" -H "Authorization: Bearer $TOKEN"

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.

curl "$BASE/api/v1/locations?pageSize=50&sort=name&direction=asc&filter=Laundry" -H "Authorization: Bearer $TOKEN"

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.

Selected columns: fields is a comma-separated list of bankName,vismaCustNo,bankActivationCode,locationActivationCode,address,zip,longitude,latitude,teltonikaSms,alternativeBankName,mask,timeZone,online,lastContactAt. Omitted or empty means identifiers, status, names and icons only; unselected item properties are null. The response fields array contains the normalized requested selection. Location-owned address and zip are strings (empty when missing), without bank inheritance; longitude and latitude are nullable decimal degrees, converted from stored microdegrees, null when missing, malformed or outside ±180/±90. Only requested settings are joined; external ID may still be read internally for search/sorting. Selecting activation codes never bypasses the existing site-wide grant and Create permissions. Example: GET /api/v1/locations?fields=vismaCustNo,address,zip,longitude,latitude&enabledOnly=true&pageSize=50. Keep the same selection on every continuation; order and duplicate names do not matter. Unknown names return 400 invalid-fields; changed selections return 400 invalid-cursor, so restart without a cursor. The portal immediately reloads the list when a column checkbox changes in its side panel and remembers the selection in this browser and the page URL; KIDs remain API identities but are no longer displayed as a list column.

The additional optional columns teltonikaSms, alternativeBankName, mask and timeZone read the location-owned eSetting.TeltonikaSMS, eSetting.Bank, eSetting.Access and eSetting.TimeZone from Log24. They are strings: selected missing values are empty, unselected values are null. Phone prefixes, mask syntax and the legacy time-zone representation are preserved (for example 100 means UTC+1); this is not an IANA time-zone ID or a calculated current daylight-saving offset. No bank inheritance or interpretation of masks as API permissions. Example: GET /api/v1/locations?fields=teltonikaSms,alternativeBankName,mask,timeZone&pageSize=50.

Additional billing columns and sort keys: vismaCrAcNo, vismaInvoiceVersion, vismaOrdre, vismaPNTurnover, vismaPNSettlement, vismaSettlement, vismaVAT, vismaServiceKey, vismaStart, vismaNote, hiddenNote, vismaGuaranteeMonth, vismaGuaranteeUnder, vismaGuarantee, vismaGuaranteeCustomer, vismaGuaranteeOver, gift, giftBegin, giftEnd, giftSplit, giftPN. Each reads the identically named eSetting from the location’s Log24, without bank inheritance. Values are stored strings: selected missing values are empty, unselected values are null. Gift and giftBegin/giftEnd sort numerically; other billing fields sort as stored text (including percentages and legacy date strings). Gift is stored in hundredths; giftBegin/giftEnd are milliseconds since 2000-01-01 UTC. The portal formats the gift amount and these dates for display. Mixed amounts and percentages are preserved without conversion. giftPN reads legacy location setting 1620, named DurationIsETA in the shared enum for unit settings; it is the welcome-gift item number, not an ETA flag. The existing vismaCustNo and zip fields provide customer number and postal code.

The optional bankName selection shows the bank name and bank icon together after the location in the portal. Core bank identity properties remain populated in the API response.

Text filtering searches names and external ID plus every selected stored column before paging. Unselected extra fields do not match, even when used for sorting. Coordinates and gift amounts also accept their decimal representation with a dot or comma; gift/contact dates accept ISO dates and dd.MM.yyyy or dd-MM-yyyy (UTC for contact times). Online accepts online/offline or 1/0. Exact KID and full activation-code matching keep their existing authorization rules.

Optional online and lastContactAt read only the trusted tenant’s Alive table, for the exact authorized location’s Log24-discovered units within RetentionDays. Only main-unit rows with Alive.UnitId = Alive.MainId contribute; child units are excluded. Online is false if any visible main unit has Offline=1, true only for a nonempty set entirely Offline=0, otherwise null. LastContactAt is the latest valid MS2000 greater than zero and not in the future, returned as UTC ISO 8601; absent contacts are null. It is last contact, not the last machine run. Orphan Alive rows do not contribute. Both fields stay null and Alive is not read when neither selected nor sorted. sort=online orders unknown/offline/online ascending; sort=lastContactAt orders chronologically, unknown first ascending and last descending. Example: GET /api/v1/locations?fields=online,lastContactAt&sort=lastContactAt&direction=desc&pageSize=50. Existing authorization and cursor rules apply. The portal shows contact times in the browser’s time zone.

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, vismaCustNo, address, zip, longitude, latitude, teltonikaSms, alternativeBankName, mask, timeZone, online, lastContactAt, bankActivationCode or locationActivationCode. direction=asc (default) or desc. All sorting applies to the complete authorized matching result, not only the loaded page. Coordinates and legacy time-zone values sort numerically; missing/invalid numeric values come first ascending and last descending. Postal codes, phone values, masks, names and external IDs use utf8mb4_general_ci text ordering before SQL LIMIT. Code sorts use numeric FlexActivation codes and require the corresponding site-wide/Create access, otherwise 403 missing-code-access. They stream all authorized matches while retaining at most pageSize+1 candidates; narrow the filter for large result sets to avoid the existing 12-second deadline. Numeric bank/location IDs break ties in the same direction. The sort setting is read even when not selected as a returned column. Example: GET /api/v1/locations?fields=zip,address&sort=zip&direction=asc&pageSize=50. Page size is 1–100, default 50; continue with unchanged parameters and nextCursor until null. Cursors expire after 15 minutes and bind caller, tenant, grants, retention and query. Restart without cursor when these change. No total count or full in-memory catalogue; concurrent renames may move rows.

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.4.1.

Resident number suggestions

GetBankUserNumberForNewUser: GET /api/v1/banks/{bankKid}/users/next-number uses the bank's first NumberFormats entry and stored NumberFormatUserIndex (default 1). GetBankNextUserNumber: GET /api/v1/banks/{bankKid}/users/next-number-after?userNumber=1420-01-0004 starts from the supplied number instead.

Both require a valid manager bearer session, Users2, User Read, User Create and a bank-wide grant. A location-only grant is insufficient. Tenant binding comes from the site; bankKid must be its canonical bank KID. These read-only operations do not reserve a number or update the index. They use the existing FlexOrm conversion, including segment increments and wraparound, testing up to nine candidates. Active occupied numbers are skipped; creation still rechecks uniqueness in its transaction. An empty number means missing configuration or no available candidate within that bounded search, not necessarily exhaustion of the whole format.

curl -H "Authorization: Bearer $TOKEN" "$API_BASE/api/v1/banks/$BANK_KID/users/next-number"
curl -G -H "Authorization: Bearer $TOKEN" --data-urlencode "userNumber=1420-01-0004" "$API_BASE/api/v1/banks/$BANK_KID/users/next-number-after"
// Example response (illustrative bank and number):
{"bankKid":"A6Q14o58Cb","number":"1420-01-0005"}

400: invalid bank or number-format (the supplied number must match segment lengths and ranges); 401: invalid session; 403: insufficient access; 503: unavailable storage or malformed stored settings, including number-format-unavailable. Handle a failed request separately from an empty suggestion. Responses are no-store. The official portal fetches the suggestion only when its side-panel card becomes visible. New operations; not yet released.