Use ordinary HTTPS/JSON (REST) for applications and client libraries, or connect an AI agent through MCP. Both use the official portal's API and the manager's existing permissions. No database access is needed.
More1 tools (beta 0.13.0)
Both operations use POST /api/v1/tenant/tools/..., a reusable manager bearer token and JSON. Requires an active manager, More1 (29), Bank Read and at least one site-bound resource grant; a bank or location grant is sufficient. More1 is a tenant-level workspace tool. The trusted site owns the tenant; no tenant or bank selector is accepted. Requests are bounded to 8 KiB, responses are no-store; do not persist tool inputs. Calculations do not change data.
ConvertToolTime — POST .../time accepts {format,value,timeZone,offset?}. Formats: ms2000, mi2000, ss2000, utc, local. Numbers are decimal strings, never JavaScript numbers. Use an explicit browser IANA zone such as Europe/Copenhagen; UTC input is ISO 8601 with Z or an offset. Local input is an offset-free ISO wall time. Epoch is 2000-01-01T00:00:00Z. The installed Kombine.Flex.Time library defines MS2000 (milliseconds), MI2000 (minutes) and SS2000 (seconds). These MI2000 values are UTC minutes, distinct from reservation Log5 local minutes.
A successful response has success=true and result:{ms2000,mi2000,ss2000,utc,local,timeZone,offset,dateInputRounded,millisecondRemainder,secondRemainder}. Integer MS2000 preserves individual milliseconds; date input truncates to the library's 2 ms precision and flags any loss. Whole seconds/minutes truncate; remainders report the lost part. MS2000 range: 0–252455615999999; reversible MI2000: 0–2147483646; SS2000: 0–2147483647. MI2000/SS2000 output is null beyond its range, avoiding wraparound. UTC and local results must fit years 1–9999. The API never uses its server-local zone.
Time validation returns HTTP 400 with success=false,error,result:null,offsetChoices. Errors: invalid-time-value, invalid-time-format, invalid-time-zone, time-out-of-range, invalid-local-time (DST gap), ambiguous-local-time and invalid-time-offset. An overlap returns both {offset,utc} choices; explicitly resubmit the chosen offset (e.g. "offset":"+01:00"). Never silently choose an occurrence.
InspectToolKid — POST .../kid accepts {"kid":"YOUR_KID"} (maximum 2048 characters). Returns {canonicalKid,normalized,fields:[{name,value,enumName,enumNumber,index}]}. Values/numbers are strings. Uses the installed Kid parser and actual writable data fields, including MainId; zero/default fields are omitted. Settings/Icons retain zero-based indexes. Scalar aliases and derived properties are not repeated. Normalized warns that the parser changed or ignored input; compare canonicalKid. Inspection does not resolve an object, prove existence or grant access.
Handle 400 invalid input or KID; 401 sign in again; 403 missing Tab/read/scope; 503 retry later. Client regeneration and packaged downloads are deferred to beta preparation.
Widgets for your website
Live examples of the same JavaScript widgets used by the anonymous portal overview. No iframe, account or API key.
Copy an example into your own page. Use the API hostname for your tenant and the matching release environment. The module creates its own Shadow DOM and loads its stylesheet, so your website CSS does not change the cards. Labels, names and default colors come from that API. The host element controls available width: cards use a 300px minimum and wrap automatically. Use machineCount: 1, 2 or 3 for distinct random running machines. Use mountProgressWidgets for a specific unit, location or bank KID.
The module sends credential-free GET requests only. These public widget reads allow external origins; manager/session operations retain their exact-origin CORS restrictions. If your website uses Content Security Policy, allow the API origin in script-src, connect-src, img-src and style-src; dynamic progress also requires inline styles, the purchase chart requires data: in img-src, and WashDoc previews require blob: in img-src. Use HTTPS and a browser with ES modules, Shadow DOM, fetch and AbortSignal.timeout. Do not embed credentials. The data is live and may be unavailable; 503 means unavailable, never zero. Each card retries automatically and marks retained values as unavailable after a failed refresh. The ready promise reports initialization errors; set unavailableText for a localized initial failure message. Call destroy() when removing or replacing a widget group. Removing a host stops its polling automatically.
Running count refreshes every 15 seconds; purchases, reservations and machine pool every minute; active users every minute (the API caches their count for five minutes). Requests share one snapshot within the page. Countdown runs locally every second using server time, retains each selected machine and shows completion for 20 seconds. Pool bounds and public fields are unchanged; images are API-generated SVGs. Amounts remain separated by currency.
GetPublicWidgetPresentation — GET /api/v1/public/widgets
Available in this source branch; unreleased. Deploy API and Web from the same release before using this on a public website.
KID: Unit / Location / Bank
Use mountProgressWidgets(host, { apiBase, kid, language }) with a canonical Unit, Location or Bank KID belonging to this API site. No login is required. Unit displays one card; Location includes all public units in that location; Bank includes all public units in every location of the bank. Deleted objects and disabled locations are excluded; disabled units and child units are included. No account data, temperature or additional settings are published. Each page contains up to 100 units; Load more appends subsequent pages. Loaded pages refresh every minute with fresh cursors. Unknown timing is labelled; completed runs remain at 100%. Pages are not a frozen snapshot.
GetPublicScopedUnitProgress — GET /api/v1/public/widgets/progress/{kid}?pageSize=100. Follow nextCursor using cursor and the same pageSize. Cursors expire after 15 minutes. 400: invalid or cross-tenant KID, page size or cursor; 404: no public unit; 503: temporarily unavailable, respect Retry-After. An empty location/bank returns an empty items array. These public GET operations allow external origins without credentials.
Public square WashDoc widget (unreleased)
GetPublicWashDocWidget — GET /api/v1/public/widgets/washdoc is a credential-free, tenant-bound publication. Use the canonical documentKid to show one document, or unitKid to follow that unit's stored WashDocId, including its last completed document. The options are mutually exclusive. Omit both to follow the most recently started currently running public machine; this is not a random pool. The API site determines the tenant; callers cannot override it.
Only non-deleted banks, locations and units in enabled locations qualify under public visibility. Visibility is rechecked through a bounded snapshot cached for up to 60 seconds. The response contains documentKid, unitKid, localized unitName/locationName, startedAtUtc, nullable endedAtUtc, finished, text-free graphSvg and refreshAfterSeconds: 60. The simplified graph shares data acquisition, legacy numeric decoding, cycle boundaries, edge data and renderers with the full graph. It gives a visual impression of curve shapes and peaks, without axes, labels, target values or a raw measurement table. It publishes no resident, payment, credential or arbitrary-setting data. Full document/table/Excel operations remain authenticated and authorized.
mountWashDocWidget from /docs/widgets.mjs renders one square card with the graph as cover, machine/location names and date/time. It loads lazily while visible, refreshes once a minute without overlapping requests, pauses when hidden and returns ready and destroy(). Destroy before replacing the host. It uses Shadow DOM and the bundled stylesheet. For Content Security Policy, allow the API origin for script-src, style-src and connect-src, plus blob: for img-src. No API key or bearer token is required. For WashDoc, ready resolves after the first visible read attempt, including when the card displays an unavailable message; it does not guarantee document data.
<div id="washdoc" style="width:300px"></div>
<script type="module">
import { mountWashDocWidget } from "https://YOUR-TENANT-API/docs/widgets.mjs";
const widget = mountWashDocWidget(document.getElementById("washdoc"), {
apiBase: "https://YOUR-TENANT-API", documentKid: "CANONICAL_DOC_KID", language: "en-GB"
});
widget.ready.catch(console.error);
// Use unitKid: "CANONICAL_UNIT_KID" instead of documentKid to follow one unit.
// Omit both KID options to follow the newest running public machine.
// widget.destroy();
</script>
HTTP 400: invalid, noncanonical, cross-tenant or conflicting KIDs; 404: no public machine/document; 422: no numeric graph or document exceeds graph limits; 503: busy or unavailable, respect Retry-After: 60. An unavailable graph is never fabricated. Responses are no-store; anonymous GET-only CORS allows external sites without credentials. Implemented locally, not deployed; release API and Web together. Packaged client regeneration remains deferred to beta preparation.
Addresses and automatic coordinates
Available in this source branch; not yet deployed. These operations support Bank, Location, Unit and resident User. Tenant, manager and installer addresses are excluded. Supply one canonical object KID from the current site. Address values are the object's own settings; parents are not inherited.
Operation ID
Method and path
Purpose
GetObjectAddress
GET /api/v1/addresses/{kid}
Read settings and revision
UpdateObjectAddress
PUT /api/v1/addresses/{kid}
Save Address and Zip, then resolve coordinates
LookupObjectCoordinates
POST /api/v1/addresses/{kid}/lookup
Deliberately resolve the current address again
SetObjectCoordinates
PUT /api/v1/addresses/{kid}/coordinates
Save a manual coordinate pair
SetObjectCoordinateProvenance
PUT /api/v1/addresses/{kid}/provenance
Change coordinate provenance
Requires an active manager, an assigned Tab (Users2 for residents), a matching resource grant, Bank Read and the object's Read permission. Unit also requires Location Read. Every write additionally requires the object's Write permission. Bank and User need a bank-wide or tenant-wide grant; location-only grants cannot edit shared bank or resident addresses. Objects and parents must exist and not be deleted; disabled locations require tenant-wide access. Rights are rechecked under storage locks before each transaction.
Use your site's HTTPS base URL as API, the returned canonical object ID as KID, and refresh the revision between commands. Both address strings are required, may be empty and are trimmed; maximum lengths are 512 and 64 characters. Control characters are rejected. A change to either Address or Zip triggers at most one coordinate lookup. A bare postal code additionally triggers one postal-city lookup, including in manual mode. The trusted site tenant selects the country: Team, Nortec and Electrolux use DK (four digits), Washco uses GB (full alphanumeric postcode, for example SW1A 1AA), and Finelec uses FI (five digits, for example 00100). Other tenants skip postal completion. The country cannot be selected by the client. Address coordinate lookups also use the mapped country as their region bias. An exact, unambiguous postal-city result is saved in Zip as, for example, 7470 Karup J. Existing city text is preserved; missing, ambiguous or failed postal results leave the input unchanged. Manual coordinates are never changed by postal completion. If the postal response lacks a city, the full address response may supply it when country and postal code match exactly. In manual mode this address lookup completes Zip only; it preserves coordinates and provenance. Neither request is retried. Use the returned zip and revision for subsequent edits. Unchanged inputs skip lookup; use the explicit lookup operation to retry. No background retries are scheduled.
The address transaction inserts both fields and clears old automatic Latitude and Longitude and sets AutoLatitudeLongitude to 0. Successful lookup inserts the new pair and UTC month (1–12) together. Provider failure leaves automatic coordinates empty. Manual provenance 30 protects the pair during address changes. Explicit lookup replaces a manual pair only on success. Concurrent edits return Superseded without overwriting newer values. History is append-only: bank Log2 for Bank/Location/Unit, bank Log7 for User, with Sync=0; success confirms storage, not device acknowledgement.
LookupObjectCoordinates also supports a Location with an empty Address: it looks up the stored Name plus Zip. Only on success is Name saved as Address in the same transaction as Latitude, Longitude and the UTC month. Failed name-based lookups leave these values untouched; missing or invalid Name/Zip returns IncompleteAddress. A concurrent rename supersedes the result. No extra request or automatic retry is added.
The response contains kid, address, zip, nullable integer latitude, longitude, autoLatitudeLongitude, revision and outcome. Coordinates are millionths of degrees, not decimal degrees; latitude range ±90000000 and longitude ±180000000, with zero valid. Outcomes: Success, Unchanged, IncompleteAddress, ManualCoordinatesPreserved, ManualCoordinatesSaved, NoResult, TransientFailure, PermanentFailure, NotConfigured and Superseded. HTTP 200 can confirm a saved address even when geocoding fails; inspect outcome.
Errors use ProblemDetails with code: 400 invalid input/KID, 401 expired or changed session, 403 missing rights, 404 missing/invisible/deleted object, 409 stale revision (address-conflict), 503 storage unavailable. Reload on 409. After 503, timeout or a lost response, read back before retrying because the address may already be committed. The location overview has an address editor and a map above Units. Generated clients and downloadable packages are synchronized at version 0.4.2.
SetObjectCoordinateProvenance changes only AutoLatitudeLongitude: 0 (unknown), 1–12 (automatic lookup month), 13 (legacy pending), 20 (legacy failure), 30 (manual). Values 1–12 require a valid stored coordinate pair; 30 may be selected before coordinates exist. It returns ProvenanceSaved and never starts a lookup or schedules retries. canWrite is a display hint from the current permission snapshot; every write independently checks permissions.
The portal saves each address or postal-code field when it loses focus. Manual selects 30; Auto selects 13. Mode selection alone does not call Google. The map reads saved coordinates every ten seconds while the page is visible and refreshes if they change. These reads never initiate geocoding. Each address change allows at most one lookup when the source is not 30; success stores the UTC month. There are no background jobs or automatic retries after failure.
Windows app downloads
The unreleased 0.2.9 app source for Android, iPhone/iPad and Windows bundles the portal UI locally and calls the tenant's Portal API directly. Normal app use requires the API and internet, but does not require Portal Web to be running. External public portal links still require Portal Web. The current Windows correction does not rebuild Android/iOS packages. The new apps have not yet been signed for customer distribution or released.
The primary page is https://{tenant}.kombine.technology/download; this API's download page serves the same content. Beta uses the matching beta hosts. No login is required to download; installing the app grants no business access. Sign in normally inside the app.
GetPortalAppDownloadPage: GET /download returns HTML in all ten portal languages according to Accept-Language (English default; Danish fallback for individual missing translations). DownloadPortalWindowsAppInstaller: GET /download/windows/{architecture}/portal.appinstaller downloads the installation/update file. DownloadPortalWindowsPackage: GET /download/windows/{architecture}/{fileName} serves its exact versioned MSIX. Architecture is x64 or arm64.
Open the downloaded file with Windows App Installer. The signed package must be trusted on the device; WebView2 Runtime and internet access are required. Update checks at launch depend on Windows and device policy. 404 means no validated release for this site's tenant/environment/architecture, or an unknown filename. The page then shows unavailable downloads; there is no cross-tenant fallback. Installer/page responses use no-store; MSIX supports byte ranges (206) and ETag (304). Client request parameters cannot select another tenant. Signed customer packages and deployment artifact delivery have not yet been configured.
Unit documents: tables, CSV, XLS and SVG
Location and bank WashDoc history (new, unreleased)
GetLocationDocuments — GET /api/v1/locations/{locationKid}/documents uses the same active manager bearer, WashDoc tab, Location Read, Unit Read, site-bound grants and RetentionDays checks as bank document search. Only authorized, visible units are returned. Use date=yyyy-MM-dd to include the whole UTC date and earlier history back to 2000-01-01 (today by default on the location route). Repeat unitKids to select one or several canonical units in that location, up to 255; omitted means all visible units. A foreign or mismatched unit returns 400; a hidden/missing selected unit returns 404. Unit choices remain available even when unselected, and filtering happens before pagination.
GetBankDocuments accepts the same optional date, repeated unitKids, search, sort=activity|documentId and direction=asc|desc parameters. Without date, the existing from/through interval remains supported; combining date with from/through returns 400. Optional locationKid limits the bank list to one location. search is an exact numeric Doc ID or canonical document KID in the selected scope. Unit names resolve localization placeholders using Accept-Language.
GET /api/v1/locations/LOCATION_KID/documents?date=2026-10-23&unitKids=UNIT_KID_1&unitKids=UNIT_KID_2&limit=25&offset=0
GET /api/v1/locations/LOCATION_KID/documents/count?date=2026-10-23&unitKids=UNIT_KID_1&unitKids=UNIT_KID_2
GET /api/v1/banks/BANK_KID/documents?date=2026-10-23&locationKid=LOCATION_KID
GET /api/v1/banks/BANK_KID/documents/count?date=2026-10-23
All requests need Authorization: Bearer ACCESS_TOKEN. GetLocationDocumentsCount and GetBankDocumentsCount return {count, from, through} using the same filters and authorization. Count distinct location/unit/document identities, not TagId alone. Fetch totals separately without traversing pages; a pending/failed count must not block rows. Lists return the existing BankDocumentPage shape (items, locations, units, from, through, offset, limit, hasMore). Paging is 1–100 rows (default 25), offsets 0–100000; reset offset after changing filters. No measurements are read. Existing 1000-location/5000-unit/35000-metadata-row bounds and twelve-second storage deadline remain; 422 means narrow scope, 503 means unavailable/busy. In Portal, bank and location WashDoc reuse the unit list/grid, one date cutoff, automatic lazy loading and independent loaded/total count. Generated clients and packages are deferred to beta preparation.
Use a canonical Doc KID containing the site's tenant, bank, location, unit and document TagId. A unit KID, numeric document ID or foreign tenant is not accepted. These operations read one known document; they do not discover document IDs.
Search documents
GetBankDocuments: GET /api/v1/banks/{bankKid}/documents discovers document KIDs for WashDoc1/2. It requires the same bearer, WashDoc Tab, Location Read, Unit Read and matching resource grants as the document endpoints. Only authorized, retention-visible locations and units appear in results and filter choices.
Optional locationKid and unitKid restrict the bank scope; a unit also selects its location. from/through are inclusive ISO 8601 timestamps with an explicit UTC offset, defaulting to the last 24 hours, with a maximum 31-day interval. Matching documents have a tagged Cycle setting inside that interval. lastActivityUtc means the latest matching Cycle, not the full start/end; the table provides full document bounds.
The JSON response contains items (kid, locationKid, unitKid, locationName, unitName, unitIconKid, unitType, lastActivityUtc), locations, units, effective from/through, offset, limit and hasMore. Units belong to the selected location scope. Read pages lazily with limit 1–100 (default 25) and offset 0–100000. Results sort by latest activity descending, then location/unit/document identity; live changes can shift pages, so reset offset to refresh. No total count or measurements are loaded by search.
400 means invalid filters; 401 sign in again; 403 no access; 404 selected object is hidden/missing; 422 narrow by location (at most 1000 locations, 5000 discoverable units, 35000 current-setting rows); 503 unavailable/busy, with no automatic retry loop. Searches have a 12-second storage deadline, two concurrent searches per API process and a one-second queue wait. Responses use no-store. The portal uses the shared list/grid described above, with authenticated Excel download and the interactive graph. List timestamps follow the browser time zone; date cutoffs use UTC.
WashDoc3: documents for one unit (unreleased)
GetUnitDocuments — GET /api/v1/units/{unitKid}/documents lists documents for exactly the canonical Unit KID in the site's tenant. Requires an active manager bearer session, specifically WashDoc3 (55), Location Read, Unit Read and a matching resource grant. Location and unit visibility follow RetentionDays before paging; hidden or missing objects return 404. The metadata and history reads are both restricted to this unit.
Use date=yyyy-MM-dd (UTC; defaults to today) to include documents from that entire day and earlier, back to 2000-01-01. There is no 31-day history-window limit for this exact-unit list. The previous from/through request parameters are removed and return 400; bank-wide searches retain their existing interval contract. Use sort=activity|documentId and direction=asc|desc (activity descending by default). Activity is the latest matching tagged Cycle at or before the selected day's end, returned as lastActivityUtc; it is not a document start/end timestamp. Document IDs sort numerically; activity ties use document identity. Use the item's canonical kid unchanged with the existing table, graph and download endpoints and their access checks.
Response: items uses the same item fields as GetBankDocuments; effective from is the UTC epoch and through is the selected day at 23:59:59.999Z. sort, direction, offset, limit and hasMore describe the page. No location/unit filter choices, total count or measurements are loaded. limit is 1–100 (default 25), offset 0–100000. Fetch more pages as needed with the same date and sort; reset offset after changes or refreshing because live changes can shift pages. A visible unit with no matches returns an empty items array.
GetUnitDocumentsCount — GET /api/v1/units/{unitKid}/documents/count?date=2026-10-21 separately returns {count, from, through} for the same date, tenant, unit and access rules. It counts distinct document identities directly without iterating pages or loading measurements. Start this request alongside the first page and show loaded rows while the total is pending; a failed count must not hide the list. Zero is a valid total. Counts and pages use separate snapshots, so a running machine can change the total; refresh both to reconcile. Keep date identical for both requests.
Errors use ProblemDetails with code: 400 invalid KID/time/sort/page, 401 sign in again, 403 missing permissions, 404 hidden/missing object, 422 oversized metadata, 503 document-unavailable or document-busy; retry manually. Reads have a 12-second storage deadline, at most two concurrent Unit list searches plus two independent counts per API process and a one-second queue wait. Responses use no-store. Generated clients and downloadable packages will be synchronized during beta preparation.
Operation
GET path
Output
GetUnitDocumentTable
/api/v1/documents/{documentKid}/table
JSON table and metadata
GetUnitDocumentHtml
/api/v1/documents/{documentKid}/table.html
Printable HTML table
DownloadUnitDocumentCsv
/api/v1/documents/{documentKid}/table.csv
UTF-8 CSV
DownloadUnitDocumentXls
/api/v1/documents/{documentKid}/table.xls
Excel 97–2003 binary workbook
GetUnitDocumentSvg
/api/v1/documents/{documentKid}/graph.svg
SVG charts
GetUnitDocumentSvg2
/api/v1/documents/{documentKid}/graph2.svg
Responsive WashDoc chart (unreleased)
GetUnitDocumentIcon
/api/v1/documents/{documentKid}/icon.svg
Text-free visual preview (unreleased)
GetUnitDocumentGraph
/api/v1/documents/{documentKid}/graph
Semantic chart JSON (unreleased)
GetUnitDocumentPng
/api/v1/documents/{documentKid}/graph.png
Server-rendered PNG (unreleased)
GetUnitDocumentNeighbors
/api/v1/documents/{documentKid}/neighbors
Previous and next numeric Doc ID (unreleased)
GetUnitDocumentNeighbors uses the same active manager bearer session, WashDoc1/2/3 tab, Location/Unit Read, tenant, resource scope and RetentionDays checks as full documents. It returns {previous, next}; each existing neighbor has a canonical kid and lastActivityUtc (latest tagged Cycle timestamp). Missing neighbors are null and their navigation widgets should be hidden. Doc IDs count down: previous is the nearest higher numeric Doc ID (older document), and next is the nearest lower numeric Doc ID (newer document). The search covers this exact unit's full stored history, skipping gaps and ignoring list dates, ordering and filters. No measurement data is loaded. Responses are uncached and bounded by a 12-second storage deadline. Handle 400 by correcting the document KID, 401/403 by checking session/access, 404 as a missing/hidden location, unit or selected document, and 503 with a manual retry. Client package updates are deferred to beta preparation.
GetUnitDocumentGraph and GetUnitDocumentPng accept the canonical document KID, optional states/settings selection and the same manager bearer session, WashDoc tab, Location/Unit Read, tenant/scope/retention permissions as the other full document endpoints. No client-supplied chart configuration is required. Both reuse the bounded graph2 data acquisition, exact cycle resolution and legacy edge context; they are not public widget URLs.
Graph JSON includes documentKid, unitKid, unitName, finished, graph bounds fromMs2000/toMs2000, and exact nullable cycle bounds startedMs2000/endedMs2000. All timestamps are UTC milliseconds since 2000-01-01. series contains stable kind/name/localization, label, semantic group, role (measurement or target), optional unit, step and sparse points. Each point has ms2000, nullable finite value, isCalculated, isEdge and optional unit. Stored nulls break a curve; absent points do not. No resampling, smoothing, visual coordinate transforms or quantity inference is applied. Use only actual non-edge points for nearest-sample tooltips; retain edge points for clipped drawing. JSON scale values remain in their recorded units; a mixed-unit series has null series.unit and must not be drawn on an incorrectly shared scale. Bare legacy Level has no assumed liters or physical unit, except for the documented type-specific temperature alias below.
presentation contains the trusted original decoded unitType, kind (washer, dryer, dosing, energy or generic) and preferred groupOrder. Draw only observed groups, in that order, then retain additional observed groups. PNG and widget previews use the same type profile. For Alliance dryers 19/29/30 only, historical Level represents the second temperature: its group becomes temperature2, label/localization use Temperature2, and bare values use °C. LevelTarget/LevelUnfiltered remain separate legacyLevel measurements with unknown units. Bare Humidity uses % for these three types only. For QW washers 15/16/17/25/31, bare Spin values 0–3 are ordinal steps with no rpm unit; explicit rotation-scale JSON retains its recorded units. Unknown units are never guessed. Numeric humidity, voltage, current and frequency are supported when actually present; unavailable groups do not create empty bands. The small widget shows at most four prioritized numeric curves; a document containing only dosing events still has no numeric widget preview.
Legacy eState.Process (138) is read from Live and States history in the same bounded document window, including older untagged samples. An event has kind="state", name="Process" and retains the original eProcess byte: 1 Prewash, 2 Mainwash, 3 Rinse1, 4 Rinse2, 5 Rinsefinal, 6 Spin, 7 Done. These codes are independent of Cycle and CycleSub. Shared equivalent phase translations provide the label/localization key without changing the stored code; unknown valid byte codes remain explicit. Only actual in-window changes become phase markers and historical hover observations, in both browser and PNG rendering. Duplicate unchanged values are omitted from the marker layer while complete stored events remain available. Process.Done does not replace tagged Cycle/Doc-ID boundary resolution.
events contains stored Cycle/Process/CycleSub/Error codes with exact timestamps, enum names, shared localization keys and translated labels where available. signals contains InOutletHeat changes: normally heat bit 32, inlet bits 1/2/4/8, outlet bit 16. Alliance dryers 19/29/30 instead use heat bits 32|64 and airflow bit 16; inlet/outlet are null. Airflow is null for other types. Null flags/booleans mean unknown or inapplicable, not off; a known change holds until the next stored change. dosing contains SoapInfo product names and recorded nullable durationMs, volumeMl, massGrams, process and step. Both legacy tab-separated forms and packed amount/duration words are supported; missing quantities remain null. Internal source tags are never published.
The PNG endpoint renders the same semantic data with the local Chart.js renderer on an opaque white background, suitable for email attachments/inline images. It requires no open browser and makes no external chart-service request. PNG rendering is bounded; unavailable renderer returns 503 document-renderer-unavailable. Both endpoints reauthorize every request and use no-store; no incremental HTTP streaming is implemented. Existing limits remain: at most 16 numeric series, 31 days, 50,000 source samples, 500,000 cells and a 12-second storage deadline. Handle 400 by correcting KID/selection; 401/403 by checking login/access; 404 as missing/hidden; 422 by narrowing selection; 503 by a manual retry. Existing SVG/widget routes remain available. Client package regeneration is deferred to beta preparation.
GetUnitDocumentSvg2 uses the same bearer authentication, tenant/scope/retention checks and optional states/settings selection. Measurement bands share scales with dashed targets; stored Cycle/Process/CycleSub/Error events are marked. Doc ID selects tagged cycle history: PREPARING starts the cycle, first DONE ends it, or last tagged measurements bound an unfinished cycle. The graph adds a one-minute margin and rounds duration up to the next minute. Legacy untagged measurements are read for the exact unit in that bounded window. Nearest edge samples for observed fields are searched at most one hour beyond it and clipped; no interpolation or fabricated samples. Known legacy measurement JSON is interpreted without changing original text or converting units. At most 16 numeric series; 422 means no numeric measurements or too many series. SVG fills both viewport dimensions while text and strokes remain stable; times use UTC. The original graph remains available. Both reauthorize before separate private completed-document caches and return no-store; unfinished graphs are fresh. Handle 401/403 by checking login/access, 400 by correcting KID/fields, 404 as missing/hidden, 422 by narrowing selection and 503 by retrying manually.
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.
historicalMeasurements=true adds a fixed document-only whitelist of temperature, level, spin, energy/power, remaining-time and process-event states, independent of today's unit model. Default false for tables; graph2 and icon enable it automatically. It adds no settings or credential access and does not change saved unit types. Existing selection and document limits still apply. GetUnitDocumentIcon reuses the same authorized data/window and completed graph cache, reduces curves while preserving local peaks, and omits text, axes and targets. It is a visual preview, not a reading scale. Fetch visible icons lazily with bounded concurrency; use a generic document placeholder for 422 or failed reads. Example: replace graph2.svg with icon.svg in the bearer request above and create an image blob URL.
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. By default, only visible current-unit catalog 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.
Only columns with data anywhere in the complete authorized document are returned, including when explicitly selected. A column is empty when every cell has no numeric value and its text is null, empty or whitespace-only. Numeric zero, false and nonempty text are data. Remaining columns retain their order, and each row's cells follows that returned order; row timestamps are unchanged. An entirely empty selection has columns: [] and empty cell arrays. Build the mapping from columns on every response instead of assuming fixed positions or the presence of every requested field. CSV, XLS, HTML and the portal's Excel download use this same filtered table.
CSV uses a UTF-8 BOM, commas, quoted cells and CRLF. Potential formulas in nonnumeric text receive a leading apostrophe. CSV cannot distinguish missing/null/empty cells. XLS is genuine BIFF8, with text written as strings rather than formulas; more than 15 digits and noncanonical numeric formatting stay as text. Both include MS2000 and ISO UTC columns. HTML can be printed using the browser. PDF is not provided.
Graphs and completed-data cache
SVG is rendered directly with .NET XML, with no third-party chart library, scripts or external assets. Each numeric series has a separate labelled scale. Enum/boolean values use steps; explicit null/invalid measurements break a line. Sparse timestamps from other series do not. Select at most 16 numeric series; width accepts 480–2400 (default 1200).
Only a document with a terminal Cycle and an InSync timestamp at least two minutes beyond its full end is finished=true. Only finished SVGs are cached, on private disk outside wwwroot, for at most 24 hours. Keys include tenant-bound KID, unit type/name, ordered fields, width and renderer version. The cache holds at most 128 MiB/256 files and falls back to fresh rendering on cache I/O failure. In-progress SVGs and all tables/downloads are uncached. All HTTP responses use Cache-Control: no-store. X-Document-Complete reports completion; that header and Content-Disposition are available to configured CORS clients. An SVG URL requires bearer authentication; fetch it before creating an image blob URL.
One document, up to 31 days, 128 columns, 50,000 source samples, 500,000 cells, 16,384 characters per value and 8 million characters overall. Storage has a 12-second deadline. At most two exports/renders run concurrently per API process, with a one-second queue wait. Failures never return a successful partial document.
400: invalid KID/field selection/width. 401: sign in again. 403: missing access. 404: missing or retention-hidden object/document. 422: unsupported unit type, document too large, no numeric graph data or more than 16 series; use fewer fields when applicable. 503: storage unavailable or document-busy; show unavailable and avoid automatic retry loops. Errors use ProblemDetails with a stable code. Generated clients and downloadable packages are synchronized in version 0.4.2.
Getting started: Find the API address → log in as a manager → request /api/v1/session/me.
Available now: API availability, the public active-user count, and the signed-in manager’s name, icon, Tabs, bank/location access, and operation permissions. These are the data displayed on the portal’s current overview. Users2 lists, resident editing, activation letters and CSV export are available through the operations documented below. Other Tabs may still lack business operations. A permitted Tab does not mean its business functionality already has an API endpoint.
Use the tenant API domain: https://api.{name}.kombine.technology in production or https://beta.api.{name}.kombine.technology in beta. Names: team, electrolux, portal, washco, finelec and nortec. Each hostname is bound to server-configured tenancy. KIDs, form fields and forwarded host headers cannot change the tenant. Tokens apply only to their tenant and environment; log in separately when switching. Unknown hosts return HTTP 400. Beta currently uses the same tenant databases as production.
Resident receipts — GetUserReceipts (unreleased)
GET /api/v1/users/{userKid}/receipts uses the existing manager bearer session. Requires an active account, Users2, User Read and access to the resident's entire bank. A location-only grant is insufficient. The canonical user KID must belong to this API's configured tenant. Permissions are checked on every page through the normal manager snapshot (at most 60 seconds old); missing or retention-hidden residents are indistinguishable.
API='https://localhost:20332'
USER_KID='A6Q3CoAC1b41Dh'
curl --get "$API/api/v1/users/$USER_KID/receipts" \
-H "Authorization: Bearer $TOKEN" --data-urlencode 'offset=0'
# For an older page, use nextOffset and revision from the previous response:
curl --get "$API/api/v1/users/$USER_KID/receipts" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "offset=$NEXT_OFFSET" --data-urlencode "revision=$REVISION"
The response contains userKid, revision, items[], nullable nextOffset and periodCount=24. Each page has at most 20 whole receipts; offsets count receipts, not lines. Start at zero without a revision. Stop at nextOffset:null. Load only when the receipt section or its end becomes visible, with one request in flight; do not poll the whole history.
Each receipt contains key, local date, nullable locationKid, locationName, period (zero is current), provisional, kind (Purchase, Payment, Discount, TransferToRent), nullable currency, totalMinor, nullable vatMinor, balanceAfterMinor and lines[]. Lines provide nullable transaction kid, offset-aware occurredAt, nullable unitKid, unitName, all description texts[], amountMinor and calculated. Calculated settlement adjustments have no stored transaction KID.
Money is signed 64-bit minor units. A negative stored posting becomes a positive receipt amount; refunds retain the opposite sign. Keep currencies separate without conversion; unknown currency stays null. Receipt balances exclude unused current-period discount, unlike the current balance header. Discount applies to DKK only. VAT is included in the total and is returned only for cash-bank purchases with a valid configured rate; null is unknown/not applicable. Groups are separated by local day, location, kind, period and currency. Zero-total documents and Month lines are omitted.
History covers period zero and the latest 24 stored period numbers, further limited by accounting/surveillance retention. Deleted-resident visibility follows the manager's RetentionDays. Each request builds a bounded read-only snapshot: 12-second storage deadline, two concurrent reads per API process, at most 50,000 postings, 8 MiB of stored text and 1,000 documents per receipt. SQL uses Log tables only. There are no refund, payment, export or mutation operations in this endpoint. Generated clients are updated at beta release.
Errors: 400 invalid-user/invalid-page; 401 (may have no JSON body); 403 forbidden; 404 not-found; 409 receipts-changed; 503 receipts-too-large/storage-busy/storage-timeout/storage-unavailable. A 409 means discard loaded pages and restart at zero: never combine different revisions. On 401/403/404 clear the history and stop loading. On 503 show an error and offer manual retry, never fabricated zero totals. No oversized or failed result is silently truncated.
Live logs
GetLiveLogs — GET /api/v1/diagnostics/live-logs uses your reusable manager bearer session. Requires an active manager, Logs1, Managers Read and a whole-tenant KID grant. A bank-only grant is insufficient. The site selects the tenant; no tenant or server selector is accepted.
The response contains tenantKid, fetchedAtUtc, refreshAfterSeconds and three servers cards: portal-api, equipment-api, portal-web. Each card has errorCode (null on success) and events, newest first. Events include timestamp, level, message template, category and optional statusCode, elapsedMilliseconds, bankId, userIds and traceId. Numeric bank/user IDs are diagnostic values, not resource selectors.
Poll no faster than every 3 seconds. Every request rechecks access before reading cached data. 401 means invalid/revoked session; 403 means missing-logs-tab, missing-managers-read or missing-tenant-access; 503 means authorization storage is unavailable. Stop polling and clear displayed data on 401/403. Per-card errors are live-logs-not-configured or live-logs-unavailable; other cards remain usable.
Each process retains at most 100 sanitized events for 15 minutes, cleared at restart. Application Information events and all Warning-or-higher events are captured. Only approved diagnostic fields and the current assistant question are retained from message arguments; exception text is 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. The buffer retains all received lines; only lines with a recognized Warning-or-higher severity are eligible for shipping to Logz.io. Lines without a recognized severity are omitted from shipping. Delivery runs independently of open pages. A visible line confirms receipt from DigitalOcean, not acceptance by Logz.io. Logs1 also displays this panel; permissions remain unchanged. Forwarding is best effort, with possible gaps and duplicates after interruptions.
DigitalOcean runtime logs
GetHostingLogs: GET /api/v1/hosting/logs. Requires a manager bearer session, Logs1, Managers Read and whole-tenant access. Operator policy explicitly permits logs across tenants for the configured app. Hosting1 alone is insufficient.
environment: beta/production; application: portal-api/equipment-api/portal-web. Response: environment, application, fetchedAtUtc, lines (up to 100 plain-text lines), truncated. Poll at most every 10 seconds. Content is bounded to 256 KiB; successes and failures are cached for 10 seconds. Snapshots may overlap and are not a complete history. Render as text, never HTML. Pause freezes display while access checks continue; hiding/leaving clears data. No MySQL, build, deploy or crash logs.
400 invalid-hosting-selection; 401 invalid session; 403 missing-logs-tab, missing-managers-read or missing-tenant-access; 503 hosting-not-configured or hosting-unavailable. Clear on error and stop on 401/403. Provider credentials and signed URLs stay on the server. Access is rechecked before cache hits using the manager snapshot (up to 60 seconds old). Included in client packages 0.4.2; availability requires operator configuration.
Hosting1 — hosting metrics (unreleased)
GetHostingMetrics: GET /api/v1/hosting/metrics?environment=beta&application=portal-api&hours=24. Reuse the manager bearer session. Requires an active account, Hosting1 (81), Managers Read and a whole-tenant KID grant for the API site. Every call, including a metric cache hit, checks the manager snapshot, which is at most 60 seconds old. These are shared infrastructure measurements across customers, not the caller's individual tenant consumption.
environment: beta or production (default beta). application: portal-api, equipment-api, portal-web or mysql (default portal-api). hours: 1, 6 or 24 (default 24). Resource mappings are controlled by the API site; clients cannot supply a provider ID, tenant or URL. Beta and production can map to the same database.
The response contains environment, application, fromUtc, toUtc, fetchedAtUtc, refreshAfterSeconds, metrics, bandwidth. Each metric has name, unit, series, errorCode. Each series has component, instance, points; each point has timestampUtc and nullable numeric value. App metrics are cpu/memory in percent and restarts in count. Keep separate instances separate: restarts are the provider's series, not a calculated total for the selected window. MySQL returns cpu, memory, disk in percent using the provider's cluster average and connections (connected MySQL threads) in count. Missing provider labels use a numeric series label; it is display text, not a persistent object identifier.
For apps, bandwidth has dateUtc, bytes, errorCode. It always describes yesterday in UTC, independently of hours. Bytes is an unsigned decimal string, preserving 64-bit precision, or null when absent. For MySQL, bandwidth is null. An empty series or a null sample means missing data, never zero. Sample timestamps may lag; fetchedAtUtc identifies the snapshot, not measurement freshness.
Check every errorCode: individual failures return hosting-source-unavailable with no series (or null bytes); other measurements remain in HTTP 200. If all sources fail, the API returns 503 hosting-unavailable. Disabled or incomplete integration returns 503 hosting-not-configured. Other errors: 400 invalid-hosting-selection; 401 requires login; 403 missing-hosting-tab, missing-managers-read or missing-tenant-access. The listed 400/403/503 application errors use ProblemDetails with code. Malformed query types use standard model-validation errors; 401 can have an empty body. Provider error text and credentials are never returned.
Honor refreshAfterSeconds (60). Results and failed lookups are cached for one minute; browser responses are no-store. Pause polling while hidden, prevent overlapping requests, and clear displayed data after 401/403. Partial/empty data is not evidence of healthy infrastructure. Only the documented app and Managed MySQL metrics are available; the operation does not query SQL, change resources, expose logs or provide customer-specific billing. Generated clients and downloads will be synchronized at beta release.
TenantStatus1 — operational alerts
GetTenantStatus: GET /api/v1/tenant/status?limit=200. Reuse the manager bearer session. Requires TenantStatus1 (80), Bank Read, Location Read, Unit Read and a matching tenant/bank/location KID grant. Tenant navigation does not grant tenant-wide data access. The site selects the tenant; scope and RetentionDays for bank, location and unit are applied before limiting rows. Enabled must be exactly 1 at the location. Missing Deleted means zero; invalid or future deletions are hidden.
Offline uses the tenant Alive table (explicitly approved): UnitId = MainId, Cluster other than Test, contact between 100 days ago and one hour ago for Cash, or one day ago for other nonempty BankType values, including boundaries. AutoOutOfOrder uses Log24 OutOfOrder = AutoOutOfOrder, excludes StartSMS_60 and TimeSMS_61 and reads a positive integer error Id from the AutoOutOfOrder state JSON. Only the original packed UnitType determines type, using (value >> 1) & 255. UnitType2 is never an override or fallback. 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.2.
const response = await fetch(apiBase + '/api/v1/tenant/status?limit=200', {
headers: { Authorization: 'Bearer ' + accessToken, 'Accept-Language': 'en-GB' }
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const status = await response.json();
for (const source of status.sources) {
if (source.errorCode || source.hasMore) console.warn(source.kind, source);
}
console.table(status.items);
Lazy pages — GetTenantStatusPage (unreleased)
GET /api/v1/tenant/status/page?pageSize=25 provides the same authorized alerts in pages of 1–100 rows (default 25). The existing GetTenantStatus contract is unchanged. The response has status (the status envelope above, with only this page in items), offset, totalCount, previousCursor, nextCursor. Source counts cover the bounded snapshot, not only the page. The API reads up to 1,000 rows per source once; hasMore still warns of truncation. Later pages reuse this snapshot without rerunning the lookups.
Send a returned cursor unchanged, keeping pageSize and Accept-Language unchanged. Every page rechecks the active manager, Tab and operation permissions. Cursors are bound to manager, tenant, credential stamp, resource scope, RetentionDays and language; they never grant access. Snapshots expire after two minutes and can be evicted sooner. HTTP 400 means invalid paging input/cursor; 409 means changed context; 410 means expired/evicted snapshot. For 409/410 discard the cursor and request a fresh page; 401/403/503 have the meanings above. Responses are no-store.
Refresh without a cursor every 30 seconds. Optional anchor=Offline:UNIT_KID starts at that authorized row; offset=50 (0–1999) is the clamped fallback if the anchor has disappeared. Do not combine cursor with anchor/offset. This allows refreshing around the visible row. The portal keeps at most five 25-row chunks and requests earlier/later pages when scrolling. No historical export or unlimited scan is provided.
Select Public v1 (no login) in Swagger for anonymous status, statistics and the purchase map. Download its contract as Public OpenAPI JSON. Portal integrations v1 contains manager login and manager operations. This split does not change routes, operation IDs or access control. Clients generating public calls from the previously combined v1 document must now also import the Public document.
Purchases in the last hour
GET /api/v1/public/statistics/purchases requires no login. Its Swagger operation is GetPublicPurchases; the JavaScript client provides client.getPurchases().
Counts rows in the site's A{tenant}.Log1Hour using UserId >= eUserId.Users AND UserId <= eUserId.Guest 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–999999999, including guest purchases. No bank filter is added. The response contains currencies: [{currency, count, amount}], with one group per currency and amount = -SUM(Amount)/100 in major currency units within that group. A missing currency remains null in its own group. The top-level count equals the sum of group counts. No purchases returns count: 0 and currencies: []. There is no combined monetary total or currency conversion.
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. This anonymous widget read permits GET from any origin without credentials; protected operations keep the exact-origin CORS policy.
const response = await fetch('https://api.team.kombine.technology/api/v1/public/statistics/purchases');
if (!response.ok) throw new Error('HTTP ' + response.status);
const purchases = await response.json();
console.log(purchases.count);
for (const group of purchases.currencies) {
console.log(group.currency, group.count, group.amount);
}
Unreleased contract change: replace reads of the former top-level amount and currency with iteration over currencies. See the changelog. Packaged clients will be synchronized during beta release preparation.
Purchase chart as SVG
GetPublicPurchasesChart — GET /api/v1/public/statistics/purchases/chart/{color}.svg returns image/svg+xml without login. The API generates a transparent 320 × 80 sparkline that spans the card's inner width, showing only a line without area fill, visible numbers, axis labels or point markers. The final segment ends in an open arrowhead aligned with that segment; each wing is half the segment's length and uses the same stroke and color, without fill. Twelve five-minute purchase counts are spaced evenly from the left edge to the right edge, oldest first. The vertical scale follows the observed minimum and maximum. A constant series, including all zeroes, draws a centered flat line. Exact counts remain in the SVG description for accessibility. Intervals divide the exact rolling UTC hour from sinceUtc inclusive to measuredAtUtc exclusive, using each row's MS2000; they are not rounded to clock boundaries. Empty intervals count as zero. The chart counts purchases across currencies, not money, and uses the same purchase filters and one-minute snapshot as the JSON response. The JSON totals still include all retained qualifying Log1Hour rows, so their count can differ from the chart's sum when rows outside the chart window remain in the table.
The root svg element exposes data-counts: twelve comma-separated nonnegative integers in invariant format without grouping separators, oldest first. These are the same values used for the line and SVG description. When rendering with img, read the attribute from the fetched SVG and include the values in a localized alt description. Reuse that SVG response for the image and text; no additional data request is needed.
The required color path parameter accepts a three- or six-digit RGB hex value without #, such as 258623, or a supported SVG color name such as green. It only changes presentation. Tenant, dates, interval length and purchase filters cannot be overridden. The response is Cache-Control: no-store with X-Content-Type-Options: nosniff; refresh once a minute. Invalid color: HTTP 400 with ProblemDetails code invalid-purchase-chart-color. Unavailable data: HTTP 503 with code purchase-statistics-unavailable and Retry-After: 60; retain the previous chart or show unavailable, never a fabricated zero.
<img src="https://api.team.kombine.technology/api/v1/public/statistics/purchases/chart/258623.svg"
width="320" height="80" alt="Number of purchases per 5 minutes over the last hour.">
The SVG contains aggregated counts only. No transaction details, account identifiers or manager permissions are exposed. This endpoint is unreleased; generated clients will be synchronized during beta preparation.
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 or the guest ID 999999999 (eUserId.Guest), 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(). This anonymous widget read allows GET from external origins without credentials. Authenticated operations still require the configured exact origin.
Kombine logos — local SVG
Public, font-independent versions of the three original Kombine logos. Choose Public v1 (no login) in Swagger. No login, business KID, Tab, permission or database access is required. Rendering uses .NET XML, the original symbol geometry and locally embedded wordmark outlines, including the registered mark. No third-party graphics components, installed fonts, scripts or remote assets.
Artwork
Path prefix
Operation ID
Proportions
Symbol
/api/v1/logos/kombine
GetKombineLogo
1:1
Name only
/api/v1/logos/kombine-text
GetKombineText
590:111
Symbol and name
/api/v1/logos/kombine-logo-text
GetKombineLogoText
5:1
Each prefix has three forms: /{color}.svg, /{color}/{width}.svg and /{color}/{background}/{width}.svg. Operation IDs for the latter two append Sized and WithBackground. All parameters are path segments; no query string. SVG only, no raster-format fallback.
Symbol parts: Use /api/v1/logos/kombine/{parts}/{color}.svg, optionally followed by width or background/width before .svg. Operations: GetKombineLogoParts, GetKombineLogoPartsSized and GetKombineLogoPartsWithBackground. Parts accepts 1–4 letters N/E/S/W, case-insensitively, including combinations such as WE. N/S are vertical bars; W/E each include a horizontal bar and curved path. The original square viewBox is preserved. Invalid parts return 400 with invalid-logo-parameters. All parameters are path segments. Standard symbol routes return the complete symbol. Public access, colors, sizing and caching are unchanged. Unreleased; client regeneration is deferred to beta. Server disk cache preserves the URL path and SVG filename under a renderer/asset revision directory. Subsequent requests read the saved file, also after restart, until expiry or eviction.
GET /api/v1/logos/kombine/W/black.svg
GET /api/v1/logos/kombine/E/black.svg
GET /api/v1/logos/kombine/N/black.svg
GET /api/v1/logos/kombine/S/black.svg
GET /api/v1/logos/kombine/NS/black.svg
GET /api/v1/logos/kombine/WE/white/174d61/128.svg
GET /api/v1/logos/kombine/black.svg
GET /api/v1/logos/kombine-text/black/512.svg
GET /api/v1/logos/kombine-logo-text/white/174d61/512.svg
Colors accept 3/6-digit RGB hex without #, exact eColor names, standard color names or transparent, case-insensitively. Unknown names are rejected, not guessed. Without a width segment, the SVG has no fixed width/height: its viewBox and preserveAspectRatio="xMidYMid meet" fit and center the entire logo in the available viewport without cropping or stretching. Explicit width is an integer from 16–4096 pixels; height follows the original proportions and can be fractional. Use a sized URL or dimensions on the embedding element for a fixed size. The background is transparent unless explicitly supplied. Unlike the old raster-only background handling, an explicit background is painted directly into the SVG. Include alternative text when embedding the image.
All inputs describe a complete image. First requests render and atomically save to private disk outside wwwroot; repeated variants reuse files, including after restart if the disk remains. The cache keeps at most 512 variants/32 MiB for 24 hours; keys distinguish artwork, normalized colors, width, renderer version and the embedded asset hash. Cache I/O failure falls back to fresh rendering. Responses use image/svg+xml, a public 600-second browser cache and ETag/If-None-Match with 304.
Errors: 400 for invalid colors or width, without silent clamping. Validated parameters return code: invalid-logo-parameters; malformed integers use standard validation ProblemDetails. 404 for unknown routes/formats such as PNG. 429 when 16 logo requests are already running; retry with backoff. These are new endpoints, not aliases of KombineLogo1/KombineText1/KombineLogoText1. Generated clients and downloadable packages are synchronized in version 0.4.2.
GetLinearGradient and GetLinearGradientSized render a linear gradient over the entire canvas. Select Public v1 (no login), tag Gradients, in Swagger. Public presentation only: no login, KID, permissions or database access.
colors: 2–4 hyphen-separated colors, evenly spaced; 3/6-digit RGB hex without #, exact eColor names, standard named colors or transparent (case-insensitive). angle: finite degrees clockwise; 0 = left to right, 90 = top to bottom, 180 = right to left, 270 = bottom to top. Use a dot for decimals. Negative angles and full turns normalize modulo 360. Dimensions default to 200 × 200 or accept 16–4096 pixels each. Geometry retains the angle at the requested dimensions and spans the full rectangle.
SVG only, all inputs in the path; no scripts or external assets. Supply alternative text when embedding. Private disk cache: 24 hours, at most 512 variants/32 MiB; I/O failures fall back to rendering. HTTP: image/svg+xml, public cache for 600 seconds, ETag/If-None-Match with 304. Errors: 400 ProblemDetails for invalid inputs (invalid-gradient-parameters for rendering validation; malformed numbers use standard validation), 404 for unsupported formats/routes, 429 when 16 concurrent gradient requests are active (retry with backoff). Generated clients and packages will be synchronized at the next beta release.
GET /api/v1/gradients/linear/{colors}/{angle}.svg
GET /api/v1/gradients/linear/{colors}/{angle}/{width}x{height}.svg
GET /api/v1/gradients/linear/22aa88-ffcc33/45/800x400.svg
curl --fail --output gradient.svg "$TENANT_API/api/v1/gradients/linear/22aa88-ffcc33/45/800x400.svg"
Circles and progress — SVG
Three public visuals replace the rendering functions of CircleGradient1, CircleProgress1 and CircleRunning1. Select Public v1 (no login) in Swagger. They render only the supplied colors and percentage, with no login, business KID, permission requirement or database call. They do not read equipment status or calculate progress. Rendering uses .NET XML and local geometry; no third-party graphics components, external assets or scripts.
Visual
Operation ID
With dimensions
Angular gradient background
GetCircleGradient
GetCircleGradientSized
Percentage circle
GetCircleProgress
GetCircleProgressSized
Running indicator
GetCircleRunning
GetCircleRunningSized
GET /api/v1/circles/gradient/{colors}.svg
GET /api/v1/circles/gradient/{colors}/{width}x{height}.svg
GET /api/v1/circles/progress/{background}/{colors}/{percent}.svg
GET /api/v1/circles/progress/{background}/{colors}/{percent}/{width}x{height}.svg
GET /api/v1/circles/running/{color}.svg
GET /api/v1/circles/running/{color}/{width}x{height}.svg
GET /api/v1/circles/gradient/22aa88-ffcc33-ee4444.svg
GET /api/v1/circles/progress/f0f4f3/22aa88-ffcc33-ee4444/65/200x200.svg
GET /api/v1/circles/running/22aa88.svg
colors is a hyphen-separated list of 2–4 colors; running uses one. Colors accept 3/6-digit RGB hex without #, exact eColor names, standard named colors or transparent, case-insensitively. No numeric enum indices or substring guesses. Dimensions default to 200 × 200, or accept 16–4096 pixels each. SVG only; all parameters are in the path, without a query string.
The gradient fills the rectangular viewport, matching the old angular background (starts at the bottom, clockwise). The progress disc stays circular: integer percent 0–100 draws that many square marks clockwise from the top. Colors interpolate over the full 100-percent scale. Zero has no marks; 100 has all 100. The running semicircle rotates once per five seconds using native CSS and stops for reduced-motion preferences. Supply alternative text when embedding images.
These images contain complete, fixed presentation data, so all three may be cached on private disk for 24 hours (maximum 512 variants/32 MiB). Cache keys include normalized colors, type, percentage, dimensions and renderer version; files are atomically written outside wwwroot. Repeated requests reuse disk files. Cache I/O failures fall back to rendering. Responses use image/svg+xml, a public 600-second browser cache and ETag/If-None-Match with 304. This is separate from document graphs, which still cache only completed documents.
Errors: 400 for invalid color lists, unknown colors, noninteger/out-of-range percentage or size (not silently clamped); validated rendering errors include code: invalid-circle-parameters. Malformed integers use standard validation ProblemDetails. 404 for unsupported routes/formats such as PNG. 429 when 16 simultaneous circle requests are already active; retry with backoff. New operations, not legacy URL aliases. Generated clients and downloadable packages are synchronized in version 0.4.2.
For location responses using house, the API fills IconKid.Text with the location's LocationId. g/house displays this text centered in black inside the house, fitting it to the available space. The icon test page accepts a text value for previewing; a bare house without location context has no number.
Presentation responses use iconKid (C#: IconKid), replacing icon. Related fields are bankIconKid and unitIconKid. Use these strings unchanged, URL-encoded, in /api/v1/icon/g/{kid}.svg. The API constructs the KID: an icon alone is its exact eIcon.ToString() name; additional text, count, colour or icons use canonical Kid.ToString(). Bank/location/unit numbers are already in Text. Calendar uses today's day of month (1–31) in Europe/Copenhagen, resolved before the URL is created. Refreshing metadata after midnight gives a new filename; image caching is unchanged.
GetIconPresentation — GET /api/v1/icon/presentation?iconKid=calendar returns {"iconKid":"..."}. Public presentation only: no login, database lookup, business grant or mutation. Optional text (up to 128 characters without controls), signed Int64 count and RGB color (0–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.2.
GetIconAssetCatalog: GET /api/v1/icon/catalog/g returns a sorted JSON array of canonical eIcon names with a file directly in g (use line for that set). For a picker, intersect these names with availableIcons from the business response; missing assets should not be shown. No login or permissions are required to read the catalog, and it grants no write access. Rendering fallback from other sets is not included. Unknown sets return 404; retry 429/503 with backoff. The catalog is cached for ten minutes and reflects the deployed assets.
GetIconFromSet, GetIconImageFromSet and GetIconImageWithBackgroundFromSet render packaged local assets. No login, Tab, operation permission, database lookup or external icon server is required. Other KID fields do not select tenant data or grant permissions.
GET /api/v1/icon/{iconSet}/{kid}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{backColor}/{size}.{format}
GET /api/v1/icon/line/413132x7qE20i11Bi336699Ic.svg
GET /api/v1/icon/g/413132xE20i11Bi336699Ic/128.png
GET /api/v1/icon/line/house/white/128.jpg
kid is a canonical Kombine.Flex.Kid.ToString() value. The API reads Kid.Icons, Kid.Count (Int64), Kid.Color and Kid.Text. For example, Kid.Icons = [house, check], Count = 7, Color = 0x336699, Text = "A12" produces 413132x7qE20i11Bi336699Ic. If canonical KID parsing fails, an exact case-insensitive eIcon name is accepted with count zero, black and empty text; house and HOUSE work. Numeric enum IDs, indices and substring matches are not name fallbacks. Unknown names, malformed/noncanonical KIDs and undefined icons return 400. A valid KID without an explicit icon uses eIcon.none.
The separate count, color, text and sub segments and all routes without iconSet are removed. Choose line or g. Kid.Color uses the low 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. Nonzero counts display their full absolute value: positive counts use a white capsule with black digits; negative counts use a red capsule with white digits. Capsules have circular ends and a straight middle. The badge widens for extra digits without stretching its end caps or changing its height/font size; very long labels widen the SVG canvas. The SVG title also retains the full count. The first entry, Kid.Icons[0] (also exposed as Kid.Icon), selects the main icon; Kid.Icons[1] selects the under-icon. A missing second entry or eIcon.none omits the under-icon. An empty list uses eIcon.none as the main icon. Further entries are ignored for rendering, but all entries must be defined eIcon values. Other parameters remain limited to 128 characters. No query parameters are used.
Formats: svg, png, jpg/jpeg, gif, bmp, tif/tiff, webp, ppm, tga, ico. Size is clamped to 16–4096 pixels (ICO at most 256). SVG retains its authored viewport; size/background affect raster images. Omit the background segment for transparent raster output. Unknown formats return SVG with image/svg+xml, matching the existing endpoint. Raster notification labels use the bundled Noto Sans Bold font.
On the first request the image is rendered and saved to local disk; the same variant is then streamed from disk, including after a restart if that disk is retained. Old cache entries can be evicted; replacement hosts/deployments may have an empty cache. Responses use a ten-minute public browser cache and support ETag/Last-Modified conditional requests with 304. Handle 400 for invalid/oversized parameters, 404 for missing local assets, 429 for concurrency limits and 503 for unavailable rendering/storage; retry transient errors with backoff. No database fallback or public cache-clear route is provided.
Icon sets:g preserves the original multicolor SVGs; line uses its palette/text annotations. Main and under-icon each try the selected set first, then the other packaged sets in ordinal alphabetical order for the same identity. They may come from different sets. Unknown sets or assets absent from every local set return 404. Catalogs list only direct membership, and disk-cache identity includes the selected set. For example, /api/v1/icon/g/jaa_ckey.svg uses the line asset because g does not contain jaa_ckey.
Migration: add the main icon and optional under-icon to Kid.Icons in that order, set Count, Color and Text on the Kid, URL-encode ToString(), select the set and remove the count/color/text/sub segments. An exact eIcon name still works for black icons without text or badges. Old paths are not compatibility aliases; rebuild stored URLs and follow the public changelog. Generated clients and downloadable packages are synchronized in version 0.4.2.
Daily cleanup removes rendered variants unused for 100 days by default. The API tracks use independently of filesystem access times. Capacity limits can evict variants sooner; a later request regenerates them from the original bundled assets.
Services — fixed service identities
GetServices: GET /api/v1/services lists the concrete service values of eUserId, including those without saved settings. Range-end markers are excluded. Each item has a canonical kid, enum identity (ToString), name and iconKid. Only Name and Icon are selected from this site's A{TenantId:D4}.Log7, BankId 0. The finite catalog is returned in one response; nextCursor is null. Optional filter is a literal case-insensitive match on identity or name (maximum 128 characters); sort=identity|name, direction=asc|desc. Identity sorting uses its numeric enum value.
GetService: GET /api/v1/services/{serviceKid} returns service, hasApiKeyHash, apiKeyHash, canEdit, profileRevision and availableIcons. A service KID uses the existing Manager type, this site's tenant, bank zero and a concrete service ID. Do not construct a human manager KID or infer permissions from its type.
Permissions and editing
All calls require an active manager session, Services1 (60), independent PermissionService2.Read and a whole-tenant KID grant. Editing additionally requires Service Write. Readers receive a null apiKeyHash; only editors receive the stored hash. Treat it as sensitive and do not log it. Hashes never appear in directory responses.
Service endpoint authorization
Service accounts do not use a permission matrix, assigned KID scopes or tabs. Each endpoint explicitly decides which authenticated eUserId values it accepts: synchronization requires eUserId.ServiceSync; authenticated status accepts all supported service identities. Account state, key validity and the configured tenant remain enforced. Stored legacy access settings are preserved but are neither returned nor editable. GetService no longer returns access. Service permission, role, tab and KID assignment routes have been removed; stop calling them. Manager authorization for listing and editing services is unchanged. See the changelog. Client regeneration is deferred to beta.
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.ApiKeyHash, 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.ApiKeyHash, 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.2.
In Swagger, choose Downloads v1 under Select a definition for resident CSV, account CSV/Excel, settlement ZIP and document CSV/XLS exports. Use the same manager bearer token and permissions. The complete integration OpenAPI document still includes these operations for client tools.
Managers may share an email address if their passwords differ. If several accounts match both email and password, login keeps the active, non-deleted manager with the newest eSetting.Alive krumb MS2000 and atomically clears Password on the other matching managers. Activity comes from the krumb timestamp, never its Text value. Missing or invalid timestamps rank behind valid activity; equal timestamps, including all unknown, are resolved by the lowest UserId. Credentials and activity are re-read under transaction locks before cleanup. Accounts with a different password remain unchanged; permissions are never merged. Without an active matching account, no cleanup occurs. Read GetCurrentManager after login to identify the selected account.
No token is issued until duplicate cleanup commits. Unavailable storage, failed cleanup or more than 100 matching rows return HTTP 503. Do not automatically retry an uncertain result. Sessions belonging to accounts whose passwords were cleared become invalid; other API instances may retain their existing account snapshot for up to 60 seconds. Request and token response fields are unchanged.
Failed login attempts may be recorded as internal JSON diagnostics. Client status codes and responses are unchanged; internal reasons such as duplicate email/password pairs are not disclosed by login. Operators can correlate the request trace ID with API logs. Never include passwords or tokens in error reports.
Login and manager settings use the site's own A{TenantId:D4}.Log7, BankId 0. D4 pads the tenant ID to at least four digits: Team 166 uses A0166.Log7. The server's PortalSite:TenantId selects the tenant; there is no fallback to another tenant's manager database.
A manager must have Enabled = 1. An absent Deleted row (or SQL NULL) defaults to 0: not deleted. Present invalid Deleted values and positive deletion timestamps still block login.
1. From API address to your first login
The API base address for this tenant site is https://api.team.kombine.technology. Use the API address, not the portal address.
Examples automatically use the address of the API site serving this guide. Beta shows the beta address. Without JavaScript, the Team production address is shown; check the address before use. You cannot switch tenants by adding a field, query parameter, or header. The server determines the site and permissions.
Request
Purpose
Authentication
GET /api/v1/public/statistics/purchases
Purchase count for the last hour
None
GET /api/v1/public/statistics/active-users
Active-user count for the site's tenant
None
GET /api/v1/status
Check whether the API responds. Does not check MySQL.
None
POST /api/v1/session/login
Exchange a manager email and password for a temporary access token.
Email and password in JSON
GET /api/v1/session/me
Retrieve your own profile and current permissions together.
Access token
Try it without writing a program
Open Swagger and select Portal integrations v1. Swagger is a web page for reading about and trying API requests.
Expand GetPortalStatus, select Try it out, then Execute. HTTP 200 means the request succeeded.
Expand LoginManager. Replace the example email and password with your manager’s credentials and select Execute. All example credentials are fictitious and cannot log in.
Copy only the accessToken value from the response, without quotation marks. Click Authorize, paste it under ManagerBearer, and authorize. Swagger adds the Bearer prefix.
Run GetCurrentManager. Its response provides the information for your portal’s profile and access cards.
A manager must be enabled and not deleted. Missing Tabs or bank access do not prevent login itself; explain the missing access in your portal.
The HTTP requests
POST https://api.team.kombine.technology/api/v1/session/login
Content-Type: application/json
Accept: application/json
{"email":"manager@example.test","password":"<your password>"}
Send the original password over HTTPS. The API verifies it; do not encode or hash it in the client.
GET https://api.team.kombine.technology/api/v1/session/me
Authorization: Bearer <your access token>
Accept: application/json
The token lasts three days (259,200 seconds) from login or renewal. Treat it as a secret string: it is not a JWT to decode. Read expiresIn instead of hard-coding the duration. Before expiry, replace it through RenewManagerSession after user activity; ordinary API calls do not extend it. An expired token requires login. No separate refresh token or single-token revocation endpoint is provided. On logout, your client discards its token; an existing copy retains its original expiry, subject to account/password revocation.
GetCurrentManager (GET /api/v1/session/me) includes optional gravatarUrl for the authenticated manager. Prefer this picture in the signed-in account menu; use iconKid when null or the image fails, including HTTP 404. The URL uses the normalized email's SHA-256 hash, 96 px, G rating and d=404; the API does not contact Gravatar. Direct browser requests disclose the email hash and client IP to Gravatar. This display hint grants no permissions and needs no manager-directory access. Unreleased.
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.2. Packaged clients do not renew automatically.
A complete PowerShell 7 example
Copy this block into PowerShell 7. The dialog asks for the manager email and password. It displays the profile without printing the password or token. For local development, trust the .NET development certificate using dotnet dev-certs https --trust.
Unreleased. RequestManagerSignUp (POST /api/v1/session/signup) requires email, name, organisation, password and confirmPassword. Name/organisation: 1–200 characters without controls; email: plain ASCII internet address, max 254. Matching passwords: 12–128 printable ASCII characters without leading/trailing spaces. Optional language: exactly lowercase en, da or es. Omit it or send null for English. Empty strings, whitespace, regional tags, uppercase and other values return HTTP 400 ValidationProblemDetails with errors.Language before account creation or email queueing. The trusted site selects the tenant; unknown fields are rejected.
POST /api/v1/session/signup
Content-Type: application/json
{"email":"person@arendt.dk","name":"Example Name","organisation":"Example Organisation","password":"YOUR_NEW_PASSWORD","confirmPassword":"YOUR_NEW_PASSWORD","language":"en"}
POST /api/v1/session/signup-confirmation
Content-Type: application/json
{"token":"TOKEN_FROM_EMAIL"}
The API allocates a never-used manager identity under the shared tenant lock and atomically stores profile/password hash, pending marker, Enabled=0 and all nine operation masks=0 in Log7 together with verification mail in Mails. The mail UserId is this manager's identity. Mail-queue failure rolls back creation. No tabs, scopes or roles are assigned; the account cannot log in before verification. 202 accepted confirms processing, not delivery. Existing addresses silently return accepted. An untouched pending account resubmitted with its correct password receives a refreshed link using the same identity; other existing accounts remain unchanged.
ConfirmManagerSignUp (POST /api/v1/session/signup-confirmation) accepts only token. Explicit POST verifies the email and activates the existing pending account; it does not create another account or choose/change a password. Proof is bound to tenant/environment and current credential/pending/Enabled revisions, expires in 30 minutes and is single-use. 200 email-verified issues no session. Self-registration itself grants no business access. Authorized administrators may separately assign or revoke access while verification is pending. Confirmation preserves those changes and grants no permissions itself. Log in normally afterwards.
400 invalid-profile or invalid-password: correct input. 400 invalid-signup-token: invalid, expired, reused or altered account proof. Request a new link with the original credentials for a pending account. 429: wait for Retry-After. 503 signup-unavailable: uncertain outcome; never automatically retry. Never log bodies, tokens or passwords. Two requests per email per 15 minutes, silently throttled. Only exact domains nortec.dk, kombinetech.com and arendt.dk currently receive proofs; others return 400 email-delivery-restricted without creation/mail. Cleanup of abandoned pending managers is not implemented yet. Clients are updated during beta preparation.
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 attempts SQL using the tenant's explicit Write connection, or its regular connection when no separate writer is configured; MySQL enforces the necessary privileges regardless of the account name; unavailable writes return 503 and must not be retried automatically.
The portal opens /my-settings from the signed-in manager icon. Integrations use the same public operations below. Except for redeeming an emailed confirmation token, all require an active, tenant-bound manager bearer session with a current credential stamp. They operate exclusively on that session's manager. Profile, email and password operations need no business Tabs, Kids or Managers Write. Changing your own tabs requires an explicit all-banks KID grant as described below. None of these operations change KID scopes, operation permissions, account state or other users. Administrative self-edit restrictions on the manager directory remain in force.
Operation ID
HTTP
GetMyManagerProfile
GET /api/v1/session/me/profile
SetMyManagerProfileField
POST /api/v1/session/me/profile/{field}
GetMyManagerTabs
GET /api/v1/session/me/tabs
SetMyManagerTab
POST /api/v1/session/me/tabs/{tabId}
RequestMyManagerEmailVerification
POST /api/v1/session/me/email-verification
ConfirmMyManagerEmail
POST /api/v1/session/me/email-confirmation
ChangeMyManagerPassword
POST /api/v1/session/me/password
Profile, icon and theme
The profile response contains kid, name, organisation, iconKid, email, emailVerified, themeMode, iconSet, retentionDays, revision, availableIcons. No password hash or internal proof is exposed. The icon list contains person icons, with an existing non-person icon first when necessary. Writes accept only the exact field names Name, Organisation, Icon, ThemeMode, IconSet and RetentionDays. Name/organisation accept strings up to 200 characters without controls; new icons must belong to the person catalog; themeMode is numeric System=0, Light=1, Dark=2. RetentionDays requires a JSON integer from 0 to 2147483647: days you may see deleted records within your existing access. 0 hides deleted records. This changes visibility, not physical deletion; Tabs, Kids and operation permissions still apply. The response field is retentionDays; missing stored values default to 10 days; invalid values return 0. Explicit 0 is preserved.
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 line; an explicitly saved g is preserved. The portal applies this choice to navigation, headings, lists and saved workspace links. Use the unchanged API-provided IconKid in /api/v1/icon/{iconSet}/{kid}.svg; the existing cross-set fallback still applies when an asset is missing. This preference requires only your active own-account session and does not grant business access.
POST /api/v1/session/me/profile/IconSet
Authorization: Bearer YOUR_MANAGER_TOKEN
Content-Type: application/json
{"revision":"REVISION_FROM_GET_MY_MANAGER_PROFILE","value":"line"}
Use the returned revision for the next save. Unsupported values or JSON types return 400; a stale revision returns 409: reread before retrying manually. Inactive sessions return 401; on 503 or a network failure keep the last confirmed preference and reread before retrying an uncertain write. Changing this preference does not change icon identities or image cache rules.
Send the last acknowledged revision with one value. A successful write returns the complete acknowledged profile and a new revision; do not mark a selection saved before this response. HTTP 409 profile-conflict requires a fresh read and a user decision before overwriting. The existing SetCurrentManagerTheme operation also remains available.
GetMyManagerTabs returns kid, tabs, availableTabs, revision, canEdit; each tab has a stable numeric id and enum-derived name. Any active manager can read their own selection. canEdit requires an explicit grant for the site's whole tenant (all banks and locations). Individual bank/location grants do not qualify, even if they cover every current bank. No Managers tab or Managers Write is required. Tabs do not grant operation permissions, and unimplemented tabs remain hidden from the workspace.
Choose a tabId from availableTabs, then send only boolean enabled and the last tab revision (64 hexadecimal characters). This revision is separate from the profile revision. The session identifies the manager; no manager/tenant selector is accepted. The current credential and all-banks grant are rechecked inside the Log7 transaction. Unknown numeric stored tabs are preserved, and unchanged selections create no history. Any own tab, including Managers, can be removed and restored while all-banks access remains.
GET /api/v1/session/me/tabs
Authorization: Bearer ACCESS_TOKEN
POST /api/v1/session/me/tabs/60
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"revision":"REVISION_FROM_GET_MY_MANAGER_TABS","enabled":true}
HTTP 200 returns the complete acknowledged tab response. HTTP 400 means an invalid request/tab; 401 means an invalid or revoked session; 403 missing-tenant-access means the all-banks grant is absent; 409 tabs-conflict or invalid-stored-tabs requires a fresh read and a user decision. A network error or 503 can have an uncertain outcome: never retry automatically. This instance invalidates its permission cache immediately; other instances refresh within 60 seconds. The portal refreshes workspace tabs immediately after the confirmed save, including open bank branches, without replacing personal-setting drafts. If navigation cannot be refreshed, the saved selection is retained and a Reload link is shown.
Verified email changes
Request verification with the new email and the current password, including for local development sessions. Optional language is en (default), da or es. HTTP 202 email-verification-queued confirms the queue commit, not delivery. The existing sign-in address remains unchanged until an explicit confirmation POST. The same flow can verify an existing address.
POST /api/v1/session/me/email-verification
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"email":"alex@nortec.dk","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 jens@arendt.dk, 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.2. These endpoints are new; no existing contract was renamed.
Forgotten passwords
RequestManagerPasswordReset and ResetManagerPassword are anonymous HTTPS operations. The API host selects the tenant. Recovery works only for one unambiguous, active manager account; it never grants Tabs, Kids or operation permissions.
Temporary development delivery restriction: a single plain email address on exactly nortec.dk, kombinetech.com or arendt.dk receives directly. All other recipients are replaced with jens@arendt.dk. 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":"manager@example.com","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.2.
Manager invitations
InviteManager sends an existing manager an invitation to choose a password. Use an active manager bearer session with Managers1 (28), Managers Read and Write, and tenant-wide access. The own-profile lock also applies: editing your own account requires being the sole active manager with tenant-wide access. The API rechecks current credentials, permissions and target visibility inside the queue transaction.
POST /api/v1/managers/{managerKid}/invitation
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"expectedRevision":"PROFILE_REVISION_FROM_GET_MANAGER","language":"en"}
Replace expectedRevision with the 64-character profileRevision returned by GetManager or the latest confirmed profile save. The target must be enabled, not deleted and have a valid saved email address. The request cannot override the recipient or portal URL. Language is en (default), da or es.
HTTP 202 {"code":"invitation-queued"} confirms the mail queue commit, not delivery. The same recipient restriction applies to invitations: only nortec.dk, kombinetech.com and arendt.dk receive directly; every other recipient goes to jens@arendt.dk, 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.2.
Client IP for server-side login
The API records the connection's IP address for rejected login attempts. A portal calling the API from its own server can additionally send its browser client's IPv4/IPv6 address in the optional X-Portal-Login-Client-IP header:
X-Portal-Login-Client-IP: 192.0.4.20
Send only an address observed by the portal server itself. The API treats this header as caller-reported metadata and records it separately from its own observed IP. It grants no permissions and does not change tenant binding, login restrictions or rate limits. Invalid or multiple addresses are ignored without changing the login response. Direct API clients do not need this header. Login responses and error handling remain unchanged; no diagnostics are returned in the response.
2. Understand the profile and permissions
This response is fictitious. Always use the actual values returned by the API.
Optional organisation from the manager's eSetting.Organisation in A{TenantId:D4}.Log7, BankId 0. Empty string when absent. The portal displays it below the name when nonblank. Display only; grants no access. Read in the same query and included in the manager's cache of at most 60 seconds.
retentionDays
Days after deletion during which the manager may see a deleted bank, location, unit, user or reservation. Read from eSetting.RetentionDays in A{TenantId:D4}.Log7, BankId 0. Missing, negative or invalid values default to 0, hiding deleted objects. This does not expand Kids, Tab or operation permissions and does not schedule physical deletion. Cached with the manager for at most 60 seconds. GetBankUsers enforces this limit. Future details, counts and searches must enforce it too.
kid
The manager ID as a string. Store and pass it unchanged. me returns only the signed-in manager. An ID does not grant access.
name, iconKid
Display name and icon identifier. The name can be empty. house can be displayed from /api/v1/icon/g/house.svg. Use a known valid icon name or a default icon; never insert raw markup or an arbitrary URL from the field.
tabs, tabDetails
Permitted pages, called Tabs. IDs correspond to eTab; names are stable enum identifiers, not translated page titles. Use IDs as keys and translate your own display text. Do not interpret unknown IDs as known permissions.
hasBankAccess
Whether at least one bank or location grant applies on this site. This is not blanket access to data.
resourceGrants
The specific areas the manager may access, explained below. Does not contain bank names or actual bank records.
operationPermissions
Four independent operation permissions: Bank, Location, Unit, User. Use the computed canRead, canWrite, canCreate flags to display relevant actions.
Portal database access
GetCurrentManager includes databaseAccess in the same profile request. Reuse the manager bearer session; no tenant or bank selector is accepted. This field is for display, for example in the workspace footer. canWrite=true means that the read/append privilege check on the tenant's shared Log7 and bank-zero Log7 succeeded. false means reading succeeds but the SQL privilege check is denied or the server is read-only. null or an absent field means unknown, never confirmed read-only access.
checkedAtUtc is the UTC start of the check. Known results are shared for 10 minutes per tenant/API instance; failures for one minute. There are no test writes or background polling. A failed check does not turn an otherwise valid profile response into a login failure. Existing session 401/503 rules still apply. This is an indicative check: privileges may differ between banks, and triggers/transactions are not checked. Administrator exceptions to read-only are not assumed. The field never grants access; operations still require the manager's account, Tab, KID and operation permissions.
Save your theme
GetCurrentManager returns themeMode, a numeric eThemeMode: System=0, Light=1, Dark=2. Missing or invalid stored values default to System. Apply this value when signing in, including on a different browser or device. System follows the device appearance; do not save the currently resolved light/dark color as the preference.
SetCurrentManagerTheme saves only the authenticated manager's own setting. It requires an active site-bound manager session, including the current account state and credential stamp. It does not require bank Tabs, resource grants or resident-write permission and does not grant business access. No tenant, bank, manager ID or KID override is accepted.
POST https://api.team.kombine.technology/api/v1/session/me/theme
Authorization: Bearer <accessToken>
Content-Type: application/json
{"themeMode":2}
Success is HTTP 200 with {"themeMode":2}. Repeating the same stored value does not create another history record. The saved value is included in the next profile read; the writing API instance invalidates its profile cache. Other instances can retain an older profile for up to 60 seconds. Responses are no-store.
Missing/unknown modes or extra request fields return 400; expired/revoked sessions return 401. Unavailable persistence, including an unconfigured write connection, returns 503. Keep the last confirmed preference and explain a failed save; do not claim that a local browser preview was saved. After an uncertain network result, re-read the profile before retrying. There is no background preference polling or cross-device push; a later login loads the stored setting. The operation uses the same HTTPS/JSON bearer contract for official portals, external portals and authorized agents.
Which banks and locations?
Each entry in resourceGrants applies only to its tenant. Multiple entries grant access to multiple areas.
scope = "Tenant": the KID identifies the site's entire tenant. Display “All banks” and “All locations”.
scope = "Bank": the KID identifies one bank and all its locations.
scope = "Location": the KID identifies one particular location in a bank.
scope describes the extent of access. Use the ID unchanged; do not derive permissions or object names from the string.
Working with KID strings
A KID is an ID returned in a kid field. Treat it as an ordinary string in every programming language. Store and send the complete value unchanged, preserving letter case. Do not decode, split or construct it yourself. No Kombine library is required.
Use the bank kid from navigationBanks for bank requests, the location kid from the location list for location requests, and the user kid from the user list for user requests. Use encodeURIComponent(kid) in JavaScript when inserting an ID into a URL. Use name for display and scope for the extent of access.
An ID is neither a login token nor a permission. The server checks the site and manager permissions on every protected request. An ID cannot switch tenants or grant more access.
This contract revision replaces the separate userId, tenantId, bankId, and locationId fields with KID strings. Update clients that used the old fields. Existing login and profile routes are unchanged.
What may the manager do?
Installer comes from eSetting.PermissionInstaller2 (3016), read from current bank-zero Log7. It is included in GetCurrentManager, GetManagers and GetManager. GetInstallers requires Installer Read, Installers1 and tenant-wide access; these flags do not grant permissions in other categories. Match permission entries by resource, not array position or a fixed category count.
The seven categories Managers, Bank, Location, Unit, User, Installer and Service use independent ePermission2 bits from PermissionManagers2/Bank2/Location2/Unit2/User2/Installer2/Service2. flags is the bitmask: Read=1, Write=2, Create=4, Delete=8, RenameExtrenatId=16, Rename=32. For example, 5 grants reading and creation, but neither writing nor deletion. No flag implies another.
Missing/empty values default to Read (1); explicit 0 grants nothing. Invalid values or unknown bits yield null and no access. Use canRead, canWrite, canCreate, canDelete, canRenameExternalId and canRename for display. The retained level field is enum text and may be numeric for combinations. Old Permission settings are not used. Tabs, KID scope and active account checks remain required; insufficient rights return HTTP 403. Managers flags do not introduce new manager operations.
Show “You do not have access to any banks yet” when hasBankAccess is false. Show “You do not have access to any Tabs yet” when tabs is empty. Both messages may apply. A network error or HTTP 503 must be shown as a retrieval error rather than missing permissions.
Client controls help the user navigate. The API must enforce account state, site, Tab, the actual object’s scope, and the operation before returning or changing business records. Editing client JSON or a link must never grant more access.
3. Errors and client actions
Check the HTTP status first. Login can return HTTP 403 with a stable code, for example {"code":"disabled"}. Other errors normally use Problem Details with status, title, and possibly traceId; validation errors can also contain errors. A proxy or web server can return an empty body or HTML, so error handling must not require JSON.
Status / code
Meaning
Client action
400
Invalid JSON, email, or missing fields.
Correct the input. Email is limited to 254 characters; password to 1,024.
401 on login
Credentials do not match a valid manager.
Show “Incorrect email or password.” Do not retry automatically.
401 on me
Missing, invalid, or expired token, or a changed account/password.
Discard the token and return to login. This response does not reveal the exact account state.
403 / disabled
The manager is disabled.
Explain that the manager is disabled and suggest contacting the administrator.
403 / deleted
The manager is deleted.
Explain that the manager is deleted and suggest contacting the administrator.
403 / account-settings
Enabled is missing or invalid, or a present Deleted value is invalid.
Explain that account setup is incomplete and needs administrator attention.
413 / 415
Oversized login body / unsupported content type.
Send only email and password as application/json. Login body limit: 8,192 bytes.
429
Too many login attempts.
Wait at least Retry-After seconds (currently 60). Default to 60 seconds if absent.
503
Required data cannot be loaded.
Display a temporary service error. Offer a retry after a pause.
Login reveals a 403 reason only after verifying the password. Translate the codes in your own interface. Do not depend on English error titles or a particular traceId.
4. Your own portal
Complete JavaScript example
Open the working JavaScript example. It includes API address, email, and password fields and buttons for status, login, profile refresh, and logout. It uses ordinary fetch without npm packages or a framework.
To use it in your portal, copy portal-api.mjs into your JavaScript folder. It is a small optional example; direct fetch calls also work, as shown below. Create one client per user and API site:
import { createPortalClient } from './portal-api.mjs';
const portal = createPortalClient('https://api.team.kombine.technology');
// Obtain email and password from your login form.
await portal.login(email, password);
const manager = await portal.getProfile();
// Display manager.name, manager.tabDetails, and manager.operationPermissions.
// Call portal.getProfile() for manual refresh and portal.logout() for logout.
Load your own script with <script type="module" src="./app.mjs"></script>. Host the example files on your own web server; do not open them using file:// or import the module directly from another API origin. The same module works in Node.js with built-in fetch. Server-side JavaScript does not need CORS.
The client keeps only the token in memory, reuses it, clears it after HTTP 401, and provides errors with status, code, and retryAfter for HTTP 429. Network, certificate, timeout, and CORS failures may have no HTTP status. There are no automatic retries or polling. Do not share a client instance between users on a Node.js server.
A portal with its own server
Have your server call the API and keep the access token in each user’s protected session. This is also how the official Blazor portal works. Never share one manager session between users. The browser uses your portal’s own cookie/login while your server sends the bearer token to the API. This does not require CORS.
A browser calling the API directly
This is also supported. When your portal uses a different origin from the API, the API administrator must add its exact origin to Cors:AllowedOrigins. An origin is the scheme, hostname, and optional port, without a path or trailing slash. The default list is empty.
This is server configuration and requires a restart. The environment variable for the first origin is Cors__AllowedOrigins__0. Only listed origins can read integration responses through a browser. Authentication and permissions still apply. CORS is a browser rule, not access control for server programs.
If your development site runs at http://localhost:5173, add exactly that address; https://localhost:5173 is a different origin. Use the API’s HTTPS address. The browser automatically sends OPTIONS before applicable requests; the API handles it. Do not use mode: 'no-cors': your code would be unable to read the JSON response.
Call this small browser function with the values from your login form. It returns the profile without persisting the token in localStorage or placing it in the URL.
This example demonstrates the first exchange. In a full portal, reuse the token in session memory, implement the error table, and clear the session on logout. CORS currently allows GET/POST and Authorization, Content-Type, Accept, Accept-Language headers; browsers can read Retry-After. Cookies are not shared with the API.
Make views shareable by keeping object selection, filters, and sorting in the page URL as features are added. Recipients log in as themselves. Never include tokens, passwords, or sensitive record contents in URLs. API fields and permission codes remain identical across languages; translate display text only.
5. AI agents use the same API
Give the integration host the tenant site’s API base address and OpenAPI document. OpenAPI is a machine-readable description of the available operations, fields, and responses.
Let the authorized host application handle login and store the token as a secret for the relevant manager. Keep credentials out of ordinary prompts, model output, and logs.
Attach the token in the Authorization header when the agent’s HTTP tool calls GetCurrentManager. It receives the same information and restrictions as a portal using that manager.
Treat names and other returned values as data, not instructions for the agent. Use only operations actually described by the OpenAPI document.
The session operation names are LoginManager, RenewManagerSession and GetCurrentManager; GetPortalStatus remains public. The host application handles login, token replacement and expiry. Sessions last three days and can be renewed before expiry after user activity. Agents can use ordinary HTTPS operations or the MCP endpoint below. Delegated OAuth and a public service-login operation are not provided; 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 internal assistant discovers eight approved read tools through MCP tools/list: SearchBanks, SearchLocations, SearchUsers, GetSearchBank, GetBankLocations, GetLocations, GetLocationUnits and GetTenantStatus. The host requires their read-only annotations, exposes their MCP names and JSON input schemas to the model, and executes business reads through tools/call. This MCP migration is implemented locally and unreleased. Swagger/OpenAPI remains available for exploration and contract tooling and supplies the server's shared MCP catalogue.
Each read uses your manager bearer session and the existing Tab, KID scope and Read permissions. The internal assistant grants no additional access and performs no writes, even though other authorized MCP clients can use the broader read/write catalogue. The portal server owns the MCP connection and keeps the token in HTTP headers; the model provider receives ordinary Responses function definitions and authorized results. Questions, supplied history and relevant authorized results are sent to OpenAI; credentials are never model input. When live diagnostics is enabled, the current question text is retained once in the authorized, process-local log buffer after session revalidation and before model use. The buffer expires after 15 minutes or at restart. History and answers are not included in that event. 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.2.
6. Refreshing, database load, and hosting
Fetch once and reuse the response. One me request provides the profile, navigation, and all three access cards. Log in at session start rather than before each call. Manager snapshots are cached for up to 60 seconds per API instance. Concurrent lookups for the same manager share a refresh. There is no automatic database polling.
Changes to permissions, Kids, Tabs, Enabled, and Deleted can therefore take up to one minute to be observed on the next request. An idle page does not update automatically; reload it or offer a Refresh action. After cache expiry, unavailable data does not fall back to old grants. Avoid tight retry loops; for transient failures, consider waiting 5, 15, and 30 seconds, then displaying the failure.
Login currently permits 10 attempts per normalized email per minute and 120 per source IP per minute, per API instance. Server portals can share a source IP, so avoid unnecessary repeated logins. Hostnames and database details are not client credentials.
For the API host administrator
Host the API over HTTPS with the appropriate tenant binding, hostnames, database connection, and protected persistent Data Protection keys. Web and API must use the same tenant. External clients receive the API address and their manager access; database credentials stay on the server. Replicas for the same site need securely shared keys and coordinated login throttling. Caches remain local to each instance. Only trust forwarded headers from configured reverse proxies.
Swagger, OpenAPI, and the guides are included in published API output and controlled by ApiDocumentation:Enabled (default true). English is the primary documentation language at /docs (also /docs/en), with Danish at /docs/da and Spanish at /docs/es. Client packages use an English README.md and include README.da.md and README.es.md. These code changes do not themselves publish the API to the internet.
GET /api/v1/database/status is for operators only. Its separate diagnostics document requires a different credential: a JWT with scope portal.diagnostics. Manager tokens cannot access it. Use /api/v1/status or /health for normal availability checks without MySQL queries.
As the API grows
Implement new portal data and operations in the shared API before Web uses them, and document them here and in OpenAPI. External clients should tolerate additional JSON fields and future enum values without granting extra access. Do not silently change the meaning of existing v1 fields; incompatible contract changes require a new version. Large lists need server-side filtering and pagination when introduced.
Bank names in navigation
The existing GET /api/v1/session/me (GetCurrentManager) includes navigationBanks. Reuse your bearer token and read profile.navigationBanks in JavaScript. Each item has kid, name, and iconKid; no numeric identifiers or extra request are needed. Use the KID as the item identity and copy that exact string when sharing an identifier.
Labels require an active account, at least one permitted Tab, Bank Read, and a bank or location grant in this site. A location grant allows its parent bank's navigation label only, never all bank data. The array is empty for all-bank access (enumeration is deferred), no Tabs, no scopes, or invalid Bank permission. Each child Tab still requires its own authorization for actual operations.
Names and icons come from the site's Log24, EntryType Settings (2), LocationId 0, UnitId 0, using eSetting.Name (99) and Icon (37). Missing values are empty strings: show an unnamed-bank label and a local icon fallback. Render text as text, never HTML. For a safe icon identifier such as house, use /api/v1/icon/g/house.svg. The portal shows the bank KID on hover and offers a copy action on right-click.
Labels are cached for one minute per bank per API instance, shared between managers; cache misses are batched in groups of at most 64 banks. Authorization is rechecked independently with the bounded manager snapshot. No bank enumeration occurs for tenant-wide grants. A storage failure returns 503: do not interpret it as an empty permission list. Retry later; invalid/expired sessions return 401. Navigation labels do not implement bank detail or deleted-record listings.
tabDetails[].icon comes from AttributeMetaIcon on the corresponding eTab, for example bank_building. Use the same safe icon URL pattern as banks. An empty string means no icon; display a local fallback. Icons come from the shared enum without database queries.
userKid selects exactly one resident in the bank: await client.getBankUsers(bankKid, { userKid: residentKid }). Use a canonical User KID from a previous response. It must belong to the site tenant and specified bank and cannot be combined with filter or cursor (400). The same manager, Tab, User Read, location and RetentionDays checks apply. The response has zero or one item and no continuation cursors. Missing or invisible users return an empty list without revealing whether they exist. The portal shareable view uses /banks/{pageKid}, with both Tab=Users2 and the resident UserId in its bank page KID. Legacy ?resident={userKid} links redirect; browser workspace bookmarks grant no permissions.
Wildcards in filter: * matches zero or more characters and ? matches one character. Examples: filter=1568-*002* or filter=Anna?*. Matching remains substring-based in Number OR Name, including plain text without wildcards. Other characters, including SQL characters % and _, are literal. URL-encode the filter with URLSearchParams or encodeURIComponent. Matching runs in API memory against the existing cached index; no wildcard SQL is generated.
filter matches a substring in number OR name using culture-independent case-insensitive comparison. Maximum 200 characters; surrounding whitespace is trimmed. An empty filter returns the full authorized list. Filtering precedes paging and reuses the shared cached sorting index, including identity mode. Keep filter with the cursor; changing it requires a request without a cursor, otherwise 400 is returned. Permissions and RetentionDays still apply. Example: await client.getBankUsers(bankKid, { sort: 'name', direction: 'asc', filter: 'anna', pageSize: 25 }). URL-encode filter text. Shared links contain the search text.
iconKid contains the user setting eSetting.Icon (37), normalized to a valid eIcon name. Missing, empty, unknown values and none default to user. Stored enum names and numeric enum values are supported. Display it from /api/v1/icon/g/{iconKid}.svg. The icon is fetched with the page settings under the same authorization and cache; it adds no separate database query per user.
Set sort=number|name|location|deleted and direction=asc|desc. Ordering applies to the entire authorized list, including progressively loaded pages. Omitting sort preserves legacy identity order (asc only). Numeric numbers sort numerically before text numbers; names and text numbers use language-independent ordinal, case-insensitive comparison. Location means the lowest visible Access/NoAccess location number. Empty values and non-deleted users come first in ASC and last in DESC. Deleted users sort by deletion time. User identity breaks ties.
Keep sort, direction and pageSize during traversal. When changing order, restart without a cursor. A cursor for another sort or direction returns 400. Sorted cursors are positions in the current authorized list; data or permission changes can shift positions between requests. Shared links never grant the sender's permissions.
Sorting uses a shared four-setting index cached for up to 60 seconds across managers and sort choices. The API filters permissions and sorts in memory; only the selected page fetches the remaining details. There is no COUNT, SQL OFFSET or query per user. A cold cache scans the bank's ordinary users; very large banks may hit the timeout and return 503. Avoid immediate automatic retries. Identity mode retains its bounded 1,000-candidate scan and scanLimitReached behavior.
The official portal uses GetBankUsers for progressive loading while scrolling. External portals can do the same: request one nextCursor at a time, reuse the result and stop at null. Stop automatic loading on errors and offer an explicit retry. Every request is authorized, including cursors received in a shared link from a colleague.
Users in a bank (Users2)
GET /api/v1/banks/{bankKid}/users?pageSize=25 · operation GetBankUsers. Take the bank KID from navigationBanks or a Bank scope in resourceGrants. Send the manager bearer token. No separate tenant, bank or user IDs are accepted.
const page = await client.getBankUsers(bankKid, { pageSize: 25 });
for (const user of page.items) console.log(user.kid, user.name, user.number);
if (page.nextCursor) {
const next = await client.getBankUsers(bankKid, {
pageSize: 25, cursor: page.nextCursor
});
}
The response contains items, previousCursor, nextCursor and scanLimitReached. Each user has kid, name (99), number (1824), email (2800), deletedAt (1996, UTC or null), locations (1809, KID and Access/NoAccess), tags (2977, KID and eTagState) and attributes (2978, eUserAttribute name and value; negative means no numeric value). Missing text/lists are empty; missing Deleted means not deleted. Email is read from the current Log7 eSetting.Email value; both JSON strings and older plain text are decoded. This field is read-only and cannot be changed through profile.
sms is the resident’s current phone number from Log7 eSetting.SMS, decoded from a JSON string or legacy text without changing its formatting. Missing values are empty; older data may use 0 for no number. It is read with the other details under the same tab, read, resource and retention rules. This read-only field does not send texts or place calls. The portal uses mailto: and tel: for valid contact values; invalid values remain plain text. Link handling depends on the device’s mail and phone apps.
Requires an active manager, Users2 (53), User Read and a matching resource scope. Tenant/bank grants include ordinary bank users. Location grants include only users associated with at least one permitted location, in either Access or NoAccess state; other locations are removed from the response. Name, number, email, tags and attributes are shared bank-level user data. RetentionDays limits deleted-user visibility; malformed deletion settings hide the user. The inclusive eUserId.Users–UsersLast range excludes manager and service accounts.
pageSize is 1–200 (default 25). The default identity mode orders by ascending user identity. Copy each returned cursor unchanged; null means no continuation in that direction. Cursors can be shared, but never grant access. Recipients use their own permissions and may see different contents. Concurrent changes mean pages are not a frozen snapshot.
In identity mode, to limit MySQL load, batches are cached for at most 60 seconds, without a full count or OFFSET. At most 1,000 candidates are examined per request. With scanLimitReached=true, a page can be short or empty: follow its continuation cursor. Do not automatically poll every page.
400: invalid KID, cursor (restart at the first page). 401: sign in again. 403: missing Tab, scope or User Read. 503: temporary storage failure; show an error and retry later, not an empty list. Tokens are never included in shareable portal URLs. Browser JavaScript requires an allowed CORS origin as described above.
Users2: Each locations item also includes iconKid, the location eIcon name from eSetting.Icon in Log24. Missing or invalid icons default to house. Icons are cached for up to one minute. state remains Access or NoAccess. The portal displays the icon with state as a data attribute and location number/state/KID in its tooltip.
Bank overview locations
GET /api/v1/banks/{bankKid}/locations (operationId: GetBankLocations) returns an array of {kid,name,iconKid,enabled,deleted,deletedAt}. Use a plain bank KID and your manager bearer token.
Requires an active manager, at least one assigned Tab, Location Read (also included in Write/Create), and matching site/bank/location access. Location-only grants return only those locations. An explicit site-wide grant shows all location states, including disabled locations and deletions outside RetentionDays. Other grants show only Enabled exactly 1 and non-deleted locations or deletions within RetentionDays. Zero retention hides all deleted locations; malformed and future deletion values are hidden for limited grants. 400: invalid/wrong-site KID; 401: sign in again; 403: insufficient access; 503: retry later. Data is cached for up to 60 seconds; authorization is checked on every request. Results are ordered by location number, without pagination. Only locations with a Name, Icon, Deleted or Enabled setting in Log24 are discoverable. Missing or invalid icons default to house. Use kid as the location ID and name as its display name.
Status fields: enabled is true only for stored Enabled exactly 1; missing or invalid values mean false. deleted is true for a positive Deleted MS2000 value, false for zero (including missing), or null for malformed values. deletedAt is the UTC deletion timestamp, or null for zero or an unrepresentable value. Display inactive when enabled is false, otherwise deleted when deleted is true, otherwise active. Status never grants access; visibility is filtered by the API using the current manager scope and RetentionDays. These fields match GetLocations and come from the same cached Log24 read.
Location overview and units
GET /api/v1/locations/{locationKid}/units, operationId GetLocationUnits. Returns {location:{kid,name,iconKid},items:[{kid,name,iconKid,cycle,cycleText,unitType,unitTypeName,unitTypeSource}]}. Use a canonical location KID from the bank location list or a user's locations; user location entries now also include the location name.
Requires an active manager, at least one Tab, Location Read and Unit Read (Read must be explicitly set), plus access to this site and bank or exact location. Every request rechecks access. Both location and unit deletion follow RetentionDays; missing Deleted means not deleted. 400: invalid KID/site; 401: sign in again; 403: insufficient rights; 404: location missing or no longer visible; 503: retry later. At most 255 units, ordered by unit number without pagination. Empty items is a valid result.
Unit data from Log24 is cached for up to 10 seconds. Units need Name, Icon, Deleted, Cycle, UnitType or UnitType2 to be discoverable. Missing names are empty; missing/invalid unit icons use object_cube. Valid configured icons are preserved. The API includes the unit number in IconKid.Text; g/object_cube and line/object_cube display this text on the box face. The same default applies to unit overviews, documents, bookings and account entries. Web uses the shareable route /locations/{locationKid}. Workspace shortcuts are stored per manager and browser tab and never grant access; the cross only removes the shortcut.
Location weather (unreleased)
GetLocationWeather, GET /api/v1/locations/{locationKid}/weather, provides current Open-Meteo conditions through the same public Portal API used by the official portal. Reuse the manager bearer session and a canonical location KID from GetBankLocations. The trusted site determines the tenant. No provider key, address or coordinates are accepted from the caller.
Requires an active manager, at least one assigned Tab, Bank Read, Location Read, a matching site/bank/location grant and RetentionDays visibility. The location and parent bank must also pass the existing GetObjectAddress visibility checks. Neither Unit Read nor Write is required. Every request reauthorizes and reads the exact location’s own Log settings before looking up cached weather.
A complete, valid eSetting.Latitude/eSetting.Longitude pair (stored in integer millionths of degrees) takes precedence. Otherwise the API geocodes available Address and Zip using the existing site geocoder. Partial addresses and postal areas are approximate. Missing coordinates are never treated as zero or filled from a different object. This GET does not save geocoding results or change any settings.
The response contains locationKid, status, address, zip, positionSource and current. Status is Available, LocationUnknown (no resolvable position), or Unavailable (provider/configuration failure). current is null unless Available; positionSource is Coordinates, Address or null. Address and Zip may be empty. Available current data includes UTC observedAtUtc/retrievedAtUtc, temperatureCelsius, and nullable apparentTemperatureCelsius, windSpeedMetresPerSecond, precipitationMillimetres, relativeHumidityPercent, WMO weatherCode and isDay. Null means unknown. Never display missing weather as zero.
HTTP 400: invalid/foreign KID; 401: sign in again; 403: missing tab/scope/read permission; 404: missing or hidden location/parent; 503 with code location-weather-unavailable: storage failure or deadline. Keep the rest of the page usable and retry on a later navigation. Successful weather is cached for ten minutes per tenant, location and address revision; negative results for one minute. HTTP responses are no-store. Four provider lookups may run concurrently per process; the full request has a twelve-second deadline and no automatic retries. Weather refreshes on navigation, not continuously.
These are model conditions, not on-site measurements or a forecast card. Display attribution with links to Open-Meteo and CC BY 4.0. Provider fields and WMO codes follow the Open-Meteo documentation. This new operation is unreleased; packaged clients will be updated during beta preparation.
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 within the authorized Alive main terminal's plan. Child-unit opening settings and exceptions do not override it. 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.2 for this beta release.
Opening hours come from the authorized main terminal identified by the same-tenant, same-bank and same-location Alive.MainId hierarchy. Opening settings are read from that terminal in Log24. Unit-level opening overrides and eState.MasterUnitId are not used for this lookup. A missing, hidden, invalid or chained main terminal yields unknown opening hours.
Missing, empty or zero opening-time values mean no configured time restriction (AllDay). An optional custom exception with both month and day unset is ignored. Invalid date exceptions are ignored. Malformed weekly hours and unresolved main-terminal mappings remain unknown.
Location reservations (unreleased)
GetLocationReservations: GET /api/v1/locations/{locationKid}/reservations?from=2026-10-19&days=7. Use the same manager bearer session. Requires Bookings1, Location/Unit Read, matching site/bank/location scope and RetentionDays visibility. User Read is also required. Each item adds userNumber and userName from the resident latest bank Log7 settings; missing values are empty strings. Denial returns 403 before reading reservation data. Each item also includes canonical userKid (null for an unknown resident). For display only, fall back to userKid when userNumber is empty; userName remains empty when unavailable.
The response has locationKid, from, days and items [{unitKid,startMinute,durationMinutes}]. StartMinute is local wall-clock minutes since 2000-01-01; durationMinutes is positive. Use the location calendar time zone for presentation, without interpreting these coordinates as UTC. Show exact intervals, including overlaps, midnight crossings and reservations outside calendar slots. Reads the tenant's current Log5 snapshot; no bank-history fallback. Cancelled and AUT/isy entries are excluded. Weekly positions below 10080 lack a concrete date and are omitted. Dates must be 2001 or later, days 1–31. Up to 5000 rows; overflow fails rather than returning partial occupancy. 400 invalid range/KID, 401 expired session, 403 denied, 404 hidden location, 503 unavailable/oversized data. Show an unavailable state on error; an empty or failed overlay is no guarantee of availability. No writes.
Location reservation calendars
GET /api/v1/locations/{locationKid}/booking-calendars, operationId GetLocationBookingCalendars (dated contract prepared for beta), reads the opening-aware calendars of visible units. Reuse the manager bearer token and a canonical location KID from GetBankLocations. Requires an active manager, Bookings1, Location Read, Unit Read, matching site/bank/location access and RetentionDays visibility. No User Read is required; no resident or reservation data is returned. All access checks precede business-data reads.
const response = await fetch(apiBase + '/api/v1/locations/' + encodeURIComponent(locationKid) + '/booking-calendars?from=2026-10-20&days=14', {
headers: { Authorization: 'Bearer ' + accessToken, 'Accept-Language': 'en-GB' }
});
if (!response.ok) throw new Error('Calendar request failed: ' + response.status);
const { groups } = await response.json();
for (const group of groups) {
if (group.status !== 'Configured') continue; // Ignore unknown future statuses.
for (const day of group.days)
for (const slot of day.slots)
console.log(day.date, slot.startMinute, slot.durationMinutes, slot.type);
}
Dated calendars (prepared for beta)
The calendar API now combines recurring turns with location opening hours, inherited settings, holidays and date exceptions. Optional query parameters: from=2026-10-20 (ISO date; default today in the location time zone), days=14 (1–31). Invalid ranges return HTTP 400. The response adds timeZone, from and days; every group day has date and dayOfWeek. Configured groups contain the requested dates, rather than a repeating seven-day template. Slots are clipped to each date and opening interval: startMinute is 0–1439, durationMinutes is positive and startMinute + durationMinutes never exceeds 1440. Type 0=reservation, 1=chance, 2=closed, 3=unknown opening hours. Unknown must never be displayed as available. Groups also distinguish effective opening schedules. Render returned dates directly; fetch another range when navigating instead of repeating days or merging opening hours in the client. This is configuration only, not occupancy or a guarantee that a reservation can be made. Portal client packages 0.13.0 include this contract. Frontend range navigation and lifecycle checks have passed; beta deployment is pending.
Configured groups have expanded days. Malformed calendars, missing/empty calendars and the legacy no-calendar sentinel are omitted; an invalid setting on one unit does not hide valid calendars on other units. Empty groups means no visible configured calendars, not unrestricted use. At most 255 units, 8192 setting rows and an eight-second settings deadline; refresh on navigation. HTTP 400: invalid/foreign KID; 401: sign in again; 403: missing tab/scope/read permission; 404: missing or retention-hidden location; 503: unavailable, ambiguous or oversized data. Do not replace an error with an empty calendar. This endpoint does not create reservations or change settings.
Invalid date exceptions (malformed or incomplete date/time values) are ignored individually. The weekly plan and other valid exceptions remain in effect, including a lower-priority valid exception on the same date. An explicit valid closed exception is preserved.
Opening hours come from the authorized main terminal identified by the same-tenant, same-bank and same-location Alive.MainId hierarchy. Opening settings are read from that terminal in Log24. Unit-level opening overrides and eState.MasterUnitId are not used for this lookup. A missing, hidden, invalid or chained main terminal yields unknown opening hours.
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.2 for this beta release.
Workspace terminal hierarchy
terminal.kid is a canonical Controller KID with MainId and zero UnitId, distinct from the first unit's KID. The workspace hierarchy is bank → location → controller → unit; the first unit remains a separate child. GetControllerOverview — GET /api/v1/controllers/{controllerKid} and GetControllerGroup — GET /api/v1/controllers/{controllerKid}/groups/{kind}/Core reuse GetUnitOverview/GetUnitGroup authorization and retention checks, with no extra privileges. Controller data uses the first unit's Log24 storage. Responses retain that storage unit in unit.kid and identify the controller in unit.terminal.kid. Both group lists temporarily contain Core; settings use existing type metadata, while states are empty until Core state metadata exists. Do not infer state fields. Use the returned unit KID and existing revision checks for authorized setting edits/history. Invalid controller KIDs return 400; unauthorized access 401/403; unknown or hidden controllers/groups 404; unavailable storage 503. Reuse your manager bearer token:
Units returned by GetLocationUnits, GetUnitOverview, GetUnitGroup, GetUnits and GetTerminals include nullable terminal: {kid,name,iconKid}. Use the canonical terminal KID to group a child beneath its main terminal and that terminal beneath the location. Its name uses the main unit’s reported eState.ComputerName, exactly like GetTerminals, with no fallback to eSetting.Name (empty when absent), and its opaque icon KID includes the main unit number; use iconKid unchanged with GetIconFromSet.
The API reads only bounded, trusted-tenant Alive.MainId relationships after the existing manager, Tab, operation, resource and retention checks. The main terminal must be an authorized, RetentionDays-visible Log24 unit at the same bank/location. A child's positive MainId can identify it when its own Alive row is absent. An existing parent mapping to another main unit or an invalid value is rejected. No extra rights are granted and no identifiers are inferred from names, icons or Offline.
Missing, invalid, orphan, hidden, chained or unavailable hierarchy returns terminal:null. Show an unknown-terminal group in that case; do not guess that the unit is its own terminal. Optional hierarchy failure does not discard an otherwise available overview, group or directory page. Existing HTTP errors and polling limits apply. Main and child units remain in GetUnits; GetTerminals classification and paging are unchanged. Implemented locally, unreleased; packaged clients are refreshed at beta release.
On Nortec, the line-set check under-icon is rendered in the tenant green (#4AC337). The main icon color and offline cross are unchanged; tenant selection comes from trusted site configuration.
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
Progress uses only clamp((now - Started) / (Done - Started) * 100, 0, 100), rounded down to an integer. Valid times return Estimated, including 100 at and after the expected end, and remainingSeconds never falls below zero. Cycle, Enabled, OutOfOrder, Connected and sequence identifiers do not affect this calculation. Missing, nonpositive or unsupported timestamps and Done not after Started return UnknownEndTime with null percent and remainingSeconds.
Values are calculated at request time from cached inputs; calculatedAtUtc does not confirm fresh device contact. The percentage measures elapsed expected time, not physical completion. Handle 401 by signing in, 403/404 as unavailable, and 503 with a later retry.
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.close as Kid.Icons[1]; the API retains the primary icon, unit number and other presentation fields. Alive.Offline = 0 sets Kid.Icons[1] to eIcon.check, replacing any offline decoration. A missing Alive row, null or other Offline value yields offline: null and no status decoration. No MainId inference or Cycle heuristic is used.
400 rejects malformed, foreign or oversized input; 401 requires login. Per-item 403 means missing access, 404 means absent/retention-hidden, and 503 means unavailable; these items have null iconKid/offline and must not be interpreted as online. On transient failure retain the last confirmed image and retry later. Reads are bounded to the exact tenant/bank/location and cached for up to 10 seconds; HTTP responses are no-store. Refresh at most once per 10 seconds while visible, pause hidden pages and prevent overlapping requests. The public image endpoint remains anonymous and performs no Alive lookup. This adds no writes or hardware commands.
Bank icons and combined unit status
GET /api/v1/banks/icons?kid={bankKid} · operationId GetBankIcons. Accepts 1–8 canonical bank KIDs from this site; repeat kid. Requires an active manager, assigned Tab, Bank Read, Location Read, Unit Read and matching scope. Location-only grants include only those locations. Disabled locations require all-bank access; location and unit RetentionDays visibility both apply.
Any included unit with Alive.Offline=1 gives eIcon.close 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.close 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.
Only the original packed UnitType determines type, decoded with the eight-bit (value >> 1) & 255 rule. UnitType2 is ignored, including as a fallback. Missing/invalid types are null, never an invented Type000; unknown numeric types are retained.
Groups come from the release’s SettingsAndStates.json catalog: 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 catalog 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.
Price settings marked with AttributeMetaRegEx(RegExs.Price) include valueFormat: "Price" and pattern in GetUnitGroup (also GetControllerGroup). GetUnitSettingHistory returns the same valueFormat at page level. These are presentation metadata; they grant no write access. For Price, display raw value / 100 with two decimal places, and convert edited amounts back with exact decimal arithmetic (12.50 → "1250"). Apply pattern (the .NET RegExs.Price) and integer minimum/maximum to the raw value after conversion, never to displayed text. The current Price pattern accepts 1–6 digits, so raw 0–999999 represents 0.00–9999.99. SetUnitSetting still accepts and returns raw invariant text, and validates it on the server; sending "12.50" returns 400. Empty optional values stay empty. History values and restore requests stay raw; do not scale history twice. Unknown/null formats use the ordinary editor. No currency is inferred from this format. This addition is unreleased.
Live sync and setting history
hasHistory is true when the setting has at least one bank Log2 history record. Show the history control only when both canReadHistory and hasHistory are true; the flag does not grant permissions. History is still fetched on demand.
GetUnitGroup adds sync, changedBy:{kid,kind,name,iconKid} and canReadHistory for settings. Sync comes from the exact bank Log2 record matching the displayed value: only 1 is acknowledged; other values are pending, and null means unknown. A sync-only change does not change MS2000 or invalidate the value revision. Poll settings at most every five seconds (states every ten), with no overlap and a pause on hidden pages.
GetUnitSettingHistory uses the same active manager, assigned Tab, location scope, Location Read, Unit Read and RetentionDays checks. Audit labels grant no access to the editor's account or directory. Only declared current-unit settings are eligible; hidden, credential and other-scope fields are excluded.
The response is {unitKid,group,setting,items:[{value,ms2000,sync,changedBy}],nextBeforeMs2000}, newest first. Request the next page with &beforeMs2000=NEXT_CURSOR; null means the end. Limit is 1–50, default 25. Open history on demand; do not poll one history request per row. Names/icons are current Log7 labels, not historical snapshots. Manager/service labels use bank zero, installers the tenant bank and residents the unit's bank; unknown identities can have no KID/name. changedBy is null when no editor is recorded (UserId zero); display no user icon or name in that case. Unknown nonzero identities remain present.
400: invalid input/site; 401: sign in again; 403: missing read access; 404: unavailable unit/group/setting; 503: storage failure or ambiguous/oversized values. No automatic retry loop. Each call has an eight-second storage deadline and a 16,384-character value limit. No database writes, count query or history cache.
Edit a unit setting
SetUnitSetting: POST /api/v1/units/{unitKid}/groups/settings/{group}/{setting}. GetUnitGroup now also returns canEdit, revision, required, minimum, maximum and selectable options. Editing requires Unit Write as well as the read operation's account, Tab, scope and retention permissions. The API rechecks the current manager and unit type inside the write transaction.
Send invariant text in value, up to 4096 characters, and the field's expectedRevision. Type, required, range, pattern and selectable-option rules come from the enum metadata and SettingsAndStates.json. Booleans normalize to 0/1. No defaults are inserted. States are always read-only. Hidden, device-owned, read-only, ORM-managed and credential settings are not editable. MainUnit/other-object bindings and dynamic option sources remain unsupported for editing.
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
const path = `${api}/api/v1/units/${encodeURIComponent(unitKid)}/groups/settings/Core`;
const read = await fetch(path, { headers });
if (!read.ok) throw new Error(`HTTP ${read.status}`);
const field = (await read.json()).items.find(item => item.name === "Name");
if (!field?.canEdit) throw new Error("This setting cannot be edited");
const saved = await fetch(`${path}/Name`, {
method: "POST", headers,
body: JSON.stringify({ value: "Washer 1", expectedRevision: field.revision })
});
if (!saved.ok) throw new Error(`HTTP ${saved.status}; reload before retrying`);
const confirmed = await saved.json(); // value, ms2000, revision
A successful response confirms storage, not delivery to the device: it returns the canonical unit KID, group, setting, value, MS2000 and new revision. The change is appended to bank Log2 with Sync=0 and the editor's UserId; the current Log24 projection is verified before commit. 400: invalid value; 401: sign in again; 403: write denied; 404: unit/group/field unavailable; 409: value or type changed; 503: storage failure. After a conflict or lost response, read back before editing again. Never automatically replay a write.
In the portal, clicking one setting group adds all that unit's setting groups to the workspace. Clicking a state group adds all state groups. Settings save on blur and selections save immediately; the confirmed value is shown only after API success. Polling pauses during editing, saving or an unresolved error.
Unit name language
Send Accept-Language: en-GB on each request. Language is not bound to login or token. Known eLocalization placeholders such as [455] in unit names are resolved from shared resources; surrounding text and unknown IDs are preserved. Content-Language reports the selected language. The ten portal languages and regional variants are supported; no/nn map to Norwegian Bokmål and pt-BR to pt-PT. Missing, malformed or unsupported language defaults to en-GB. Quality preferences are honored and q=0 excluded. Raw data is cached before translation, so languages never mix across users and require no extra SQL.
GetLocationUnits now returns cycle (stable eCycle name) and cycleText (Accept-Language) per unit. Both are null for missing/invalid values. The newest MS2000 from eSetting.Cycle (1619, Settings) and eState.Cycle (20, States) wins; States wins ties. An invalid newest value never falls back to an older value. Unit data is now cached for at most 10 seconds; location labels and manager permissions for up to one minute. Both SQL branches are bank/location scoped. The portal refreshes every 10 seconds, pauses in hidden tabs, prevents overlapping requests and hides stale details on failure. External clients can repeat this GET with the same bearer token and Accept-Language every 10 seconds.
Settlement: viewing, history and downloads
Every call requires Authorization: Bearer TOKEN, the Settlement2 Tab, Bank Read, User Read and access to the entire bank. Location-only access cannot expose bank-wide settlement data. Permissions are checked on every request with a manager snapshot cached for at most one minute. These GET operations never close, undo or modify a settlement.
Omit beforePeriod initially. History returns up to 25 closed periods, nextSettlement and nextBeforePeriod. Pass the next cursor unchanged; null means the end. Unknown dates and values are null. Dates use UTC. Period 0 is the provisional current period, available through details and downloads; it can change until settlement.
Details contain sourceEntries, includedEntries, groups and formats. Each group has group, currency, entries and amountMinor. Amounts are signed database values in minor units, not formatted currency amounts. Different currencies are never added together. Stored history totals may differ from export totals.
ChargePoint_58/TimeNew power consumption is excluded; cash banks also exclude Month and Transfer. Group priority is ETest, EInstaller, EGuest, configured number masks (U), EDate, then LR. Masks use % for multiple characters and _ for one character. Valid legacy entries classified as Unknown retain their recorded amounts. Current user numbers, names, tags and attributes are used, so regenerating a historical file may differ from its original version. Settlement is exempt from RetentionDays: deleted users remain included in period details and downloads regardless of deletion age. Normal manager, Tab, bank and read permissions are still enforced.
The ZIP contains one file per group and currency plus a reconciliation manifest.json. Empty periods contain only the manifest. Text formats may omit groups with no exportable amounts; group totals remain in the manifest. XLS produces real .xlsx workbooks with Number, Amount and UserId following the export convention; UserId in this compatibility file is numeric, while HTTP object identifiers are KIDs. Other formats use UTF-8 text with CRLF. Excel keeps the database amount sign; NAVISION, for example, reverses it.
Formats: XLS, ATB, BL, DEAS, FRUEHØJGAARD, HEIMSTADEN, LEJERBO, MD90_1, MD90_3, MD90_3_minus, MD90_3_plus, MD90_3_AABKBH and NAVISION. MD90_3 is defined for banks 1001 and 1068 only. NIRAS and ROBERT are obsolete. HUMAN, KMD, LYKKEBO and MD90 are not offered because the reviewed shared code contains no implemented export for them. Format identifiers are case-sensitive; use the list returned in the details.
const headers = { Authorization: `Bearer ${token}` };
const base = `/api/v1/banks/${encodeURIComponent(bankKid)}/settlements`;
const details = await fetch(`${base}/12`, { headers });
if (!details.ok) throw new Error(`HTTP ${details.status}`);
const period = await details.json();
const download = await fetch(`${base}/12/download?format=XLS`, { headers });
if (!download.ok) throw new Error(`HTTP ${download.status}`);
const url = URL.createObjectURL(await download.blob());
const link = document.createElement('a');
link.href = url; link.download = 'settlement-12.zip'; link.click();
setTimeout(() => URL.revokeObjectURL(url), 60000);
This example assumes the same origin. External browser portals use the full API base address and an allowed CORS origin. 400 means invalid KID/period/format, 401 requires login, 403 means denied permissions, 404 means an unknown closed period, 422 means data cannot be exported safely, and 503 means temporarily unavailable data. Causes of 422 include invalid transaction codes, invalid numbers/currency, duplicate export numbers across users, field width overflow or more than 100,000 entries/10,000 users. No partial files are returned. Correct data/format instead of retrying 422. Back off before retrying 503.
History and period sources are cached for at most one minute, coalescing concurrent reads. Details load only when a period is selected. No background database reads or MySQL writes occur. Legacy readiness calculations and automatic jobs have not been migrated; this API does not claim a bank is ready to close.
The additional heatmap array contains {latitude, longitude, amount} for all qualifying purchases retained in Log1Hour, grouped by coordinates. There is no row limit or additional timestamp filter: the database maintains the recent-hour table. Missing/invalid coordinates are omitted; existing purchase and transfer filters remain. Amount is summed in major currency units, without currency conversion. Only animated items are limited to the newest purchases. The portal replaces the entire heatmap immediately on every ten-second refresh, independently of coin animation. No names or user identifiers are included.
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 limit applies only to animated items; heatmap data is complete. All limits share one cached snapshot; only animated items are sliced to the requested 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. The items array 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.
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 use decoded Kombine.Flex.FlexDto.KrumbValue.TransactionLine.ToString() text, including program, duration, soap and transfer details. The API resolves numeric localization placeholders. Safely displayable text-only legacy descriptions remain supported; absent DTO text uses a safe fallback. Raw JSON and payment identifiers are not exposed. 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; use paymentKind as metadata.
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 retain their safe display description and expose paymentKind as metadata; 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.2.
GetBankAccount: GET /api/v1/banks/{bankKid}/account. Requires Account2 (2), Bank/Location/Unit/User Read and a bank or location grant. The current manager account, credential stamp and site's tenant are checked before any business read. Location grants constrain rows, totals and filter options. Reuse the ordinary manager bearer session.
Dates are inclusive in timeZone, defaulting to today in Europe/Copenhagen, with daylight-saving transitions respected. At most 367 dates. A nonnegative period replaces the date interval; zero means the current period. The latest 100 available period IDs are returned. Optional locationKid, unitKid and userKid must be canonical KIDs in this bank. kind is all, debit (negative) or credit (positive). includeZero, includeBookings, includeMonthly default to true. Limit is 1–200 (default 50); offset is 0–100000. Rows are newest first, then location/unit. Separate requests may shift as new postings arrive.
items contains the immutable posting KID, scope KIDs, recordedAtUtc, signed amountMinor, currency, description, transaction type, period, current display labels, reversed, optional reversalOfKid and canReverse. totals covers the full filtered selection, separately by currency. These totals are movements, not resident balances; no currency conversion is performed. Portal amounts display legacy hundredths as amountMinor / 100. Labels are current Log7/Log24 values, not historical snapshots. Modern JSON, plain legacy text and JSON wrapped in a legacy prefix/start-code envelope are supported.
recordedAtUtc / MS2000 is event time, not insertion time. Postings can arrive minutes or days later. Never use the highest MS2000 as a watermark for all new database entries. Each page includes a revision for the entire filtered set: identities, periods and monetary totals by currency. It is independent of offset/limit and is neither a cursor nor an access grant.
GetBankAccountRevision: GET /account/revision with the same filters and permissions. Returns only {"revision":"..."}. Revision reads for identical authorized selections are shared for at most 30 seconds per API instance; every request rechecks access. Poll no faster than every 30 seconds and pause hidden views. On change, reload the loaded range from offset 0. Combine only pages with equal revisions and replace the old list only when all required pages are ready. If the revision changes during loading, discard the incomplete result and retry later with backoff.
const check = await fetch(`${accountUrl}/revision?${filters}`, {
headers: {Authorization: `Bearer ${accessToken}`}, cache: 'no-store'
});
if (check.status === 401 || check.status === 403) {
clearAccountView(); // remove data and handle login/access
} else if (!check.ok) {
showRetryLater(); // on 503, wait and increase backoff for repeated errors
} else if ((await check.json()).revision !== page.revision) {
await reloadLoadedAccountPages(); // offset 0; require equal revisions on all pages
}
Updates are not instantaneous: polling plus the shared cache can delay detection by approximately one minute. Revision checks inspect the filtered Log1 set and assume no maximum arrival delay. Names and reversal flags can change independently of the revision; reload as needed and always recheck commands on the server. Under continual changes or timeouts the old view is retained with retry available. CSV/XLSX remains a single database-snapshot export.
ExportBankAccount: GET /account/export with the same filters and format=csv or format=xlsx. Pagination is ignored. It returns the complete selection in one snapshot, at most 10000 rows and 4M description/name characters. Oversized exports fail, never silently truncate. Money columns use signed minor units, dates are UTC, identifiers/headers are stable. CSV neutralizes spreadsheet formulas; XLSX uses text cells for untrusted values.
ReverseBankAccountEntry: POST /account/{transactionKid}/reversal, without a request body. Additionally requires a bank-wide grant and Bank/User Write. Only recognized negative resident consumption with internal physical/activation tags can be reversed. The server locks and rechecks the original, appends the opposite signed amount in the current period, retains document/program metadata and requests balance/access synchronization. The original entry is preserved. Payment-provider transfers, anonymous/system users, credits, monthly transfers, unknown transaction types and already reversed entries are unsupported. canReverse is only a display hint; the command always validates again.
// Only after your own UI/user has reviewed and confirmed this exact entry.
const reversed = await fetch(`${accountUrl}/${encodeURIComponent(entry.kid)}/reversal`, {
method: 'POST', headers: {Authorization: `Bearer ${accessToken}`}
});
if (!reversed.ok) {
const problem = await reversed.json();
throw new Error(problem.code || `HTTP ${reversed.status}`);
}
Errors: 400 invalid filter/KID/time zone; 401 expired, stale or inactive session; 403 insufficient Tab/resource/operation grants; 404 missing posting; 409 already-reversed; 413 data-limit; 422 reversal-unavailable; 503 storage/write connection unavailable or nontransactional storage. Framework validation errors use the standard ProblemDetails errors object. Never automatically retry a financial write, including after timeout: refresh the list and review its result first.
Limits: read/write only Log tables; no schema changes, provider refunds, bank-to-bank transfers, balance reset or direct hardware delivery. InnoDB is required for financial writes; queued synchronization is not confirmation of delivery. At most 4096 filter labels and 100 currency groups. No caching or polling. The portal's print/PDF action prints only the loaded page; use export for the full selection.
Incremental bank search
eTab.Banks2 (5) is returned in profile tabs and tabDetails when granted to the manager in Log7. Global Search requires Bank Read, at least one current Tab and the manager's resource grants.
SearchBankActivation has a separate limit: 5 calls per 10 minutes and 20 per hour per manager in the tenant-bound API process, using fixed windows. All attempts count, including malformed and valid codes. Changing tokens or source IP does not reset the quota. HTTP 429 supplies Retry-After in seconds; wait at least that long before the next code attempt. Name searches remain available. The portal only sends code-shaped numeric queries to this provider and stops code requests during cooldown. Quotas are held in memory: restart resets them, and multiple API replicas require a shared limiter for an aggregate quota.
Call GET /api/v1/search/banks?q=Kombine (SearchBanks) and GET /api/v1/search/bank-activation?q=CODE (SearchBankActivation) concurrently with the same manager bearer session. Display each response immediately and merge items by kid. Each item contains kid, kind, name, zip and icon (the bank eSetting.Icon from the same Log24 query). Code matches have an empty icon; clients can use a default bank icon; kind is the enum name Bank. GetSearchBank: GET /api/v1/search/banks/{bankKid} resolves an authorized bank result before opening it.
Requires an active manager, at least one current Tab, PermissionBank2 Read and a matching site/bank grant. A location grant permits discovery of its parent bank only, without granting other locations. Every request checks the bounded session snapshot. The hostname determines the tenant.
Queries are trimmed and must contain 2–128 characters. Bank names and settlement emails use literal, case-insensitive substring matching. Only current bank-level Name, Icon and SettlementEmails in Log24 are read. The zip field remains empty for compatibility; banks have no postcode. Location postcodes are not searched. Values are, supporting JSON and legacy text. A text search returns at most 50 items; hasMore asks the caller to refine the query. A 60-second scope-specific cache coalesces simultaneous refreshes. The portal debounces input by 300 ms and ignores stale responses.
Bank codes are decoded with Kombine.Flex.Activation, then resolved in the same 60-second scoped Log24 cache as name searches. Results include the bank name and icon; zip remains empty. Invalid or unauthorized codes skip the bank lookup. Missing banks return no items. Bank, location and resident results are implemented. Errors: 400 query/KID, 401 session, 403 permission, 503 temporary storage failure. Retain other provider results on failure; avoid immediate retry loops.
SearchBanks also searches eSetting.SettlementEmails in Log24. Email search requires a whole-bank or tenant grant; location-only access permits bank-name discovery only. Results include matchedSetting (Name or SettlementEmails) and matchedValue containing the name or first matching email in the semicolon-separated list. Name matches take precedence. Render as text, never HTML. Example: GET /api/v1/search/banks?q=accounts%40example.test. The same cache, limits and error statuses apply; typing does not issue a new SQL query per keystroke.
Location search
GET /api/v1/search/locations?q=0123 (SearchLocations) runs independently of SearchBanks. It searches Log24 Name, Bank (alternative bank name), Zip, Address, VismaCustNo and TeltonikaSMS. Requires manager bearer, a current Tab, Location Read and a matching location grant. RetentionDays applies. Trimmed query 2–128 characters, literal case-insensitive substrings, at most 50 items plus hasMore. Results contain kind=Location, canonical kid, name, icon, zip and matchedSetting/matchedValue, with field priority in the order above. Render as text. Example: HTTPS GET with Authorization: Bearer and a URL-encoded q value.
GET /api/v1/search/location-activation?q=... (SearchLocationActivation) decodes via Kombine.Flex.Activation and resolves name/icon in the location cache. Location codes contain bank/location, not tenant; only the configured site is searched. Invalid, absent or inaccessible locations return no items. Bank and location codes share 5 calls per 10 minutes and 20 per hour per manager. Every code call counts; 429 includes Retry-After. The browser selects one code provider per input. 400: input; 401: reauthenticate; 403: permission; 503: temporary failure. Preserve other partial results on error. The 60-second cache coalesces misses; no query per location. Rate limits remain per-process and require shared storage for multiple replicas.
All bank and location searches, including activation lookups and GetSearchBank, include only BankId >= 1000. Special banks below 1000 are excluded.
Resident search
Resident results also include number from eSetting.Number. The portal displays Number · Name, omitting empty or duplicate parts. name retains its existing meaning; number is null for other result types.
Resident search and resident activation-code search also return parent banks with isContext=true when Bank Read is granted. At most 50 resident matches plus distinct banks; hasMore counts residents. Merge banks by kid with other results, including when direct bank search is disabled. Context banks do not include contact data.
A complete email address uses an exact case-insensitive match through the Log7 text index. Only names and TagIds support partial matching.
GET /api/v1/search/users?q=anna (SearchUsers) runs independently of banks/locations. Use Authorization: Bearer. Searches current Log7 Number, Name, Email, SMS and Tags. Number and email require exact case-insensitive matches; names allow literal substrings. TagId allows partial decimal IDs and spaces, but not hexadecimal. Name/TagId queries allow 10 seconds SQL and 12 seconds including queue time. Tags use the resident-list JSON/legacy parser.
Requires Users2, User Read and tenant/bank access or an Access/NoAccess association with a granted location. Ordinary users only, BankId >= 1000, RetentionDays applies. Maximum 50 visible results; up to 501 candidates examined. hasMore requests refinement when either bound is reached, so broad queries may omit matches. Returns kind=User, kid, name (number fallback), icon and matchedSetting/matchedValue, not other contact fields or tag lists. Priority: Number, Name, Email, SMS, Tags.
GET /api/v1/search/user-activation?q=... (SearchUserActivation) decodes via Kombine.Flex.Activation and resolves name/icon. Codes contain bank/user, not tenant; only the configured site is searched. All three code providers share 5 calls/10 minutes and 20/hour per manager. Invalid/invisible codes return no items. Errors: 400 input; 401 login; 403 access; 429 wait Retry-After; 503 temporary failure. Preserve other results. Cache 60 seconds per query/bank scope coalesces calls; no per-resident SQL. Quota remains per-process.
SearchLocations and SearchLocationActivation also return parent banks when the manager has Bank Read. Banks have isContext=true and contain name/icon, not contact data. At most 50 locations plus their distinct banks; hasMore counts locations. Merge by kid across responses, place each bank before its locations, and prefer direct bank matches over context explanations. Banks reuse the same 60-second cache as bank search. They are included even when only location search is enabled.
for (const item of response.items) {
if (!byKid.has(item.kid) || !item.isContext) byKid.set(item.kid, item);
}
SearchUserSms
GET /api/v1/search/user-sms?q=51573605 (SearchUserSms) is an independent fast provider with the same authorization, retention and parent-bank context as SearchUsers. It uses exact Log7.Text index lookups for plain and JSON values, normalizing spaces, hyphens, parentheses, leading + and international 00. Eight Danish digits match with and without 45. Accepts 8–15 digits; invalid format returns no items. Merge by kid. An independent 60-second cache and gate prevent slow substring/TagId queries from blocking SMS results. SearchUsers retains substring and TagId search and may still time out; keep SMS results on 503. Same 400/401/403/503 handling as SearchUsers; no activation quota is consumed.
Account endpoints retain HTTP 403 and code forbidden. After tab and bank-scope checks, the response may also include reason: missing-bank-read, missing-location-read, missing-unit-read or missing-user-read. Show a localized explanation; retain a generic denial for unknown reasons. No account data is read on denial.
Bookings: HTTP 403 retains code forbidden. After tab and bank-scope authorization, reason may be missing-location-read, missing-unit-read or missing-user-read. Localize the reason; use generic denial for unknown reasons. No booking data is read on denial.
GetCurrentManager returns Tabs and TabDetails ordered by eTab AttributeMetaSortOrder, then numeric tab ID. Missing metadata defaults to zero. Clients can display tabs in response order; permissions are unchanged.
SearchBanks, SearchLocations and SearchUsers accept complete canonical Kids and ToBankId (166.2000), ToLocationId (166.2000.4), ToUserId (166.2000-1100). Kid matches are exact, with matchedSetting=Kid and the readable identity in matchedValue. Existing site, tab, read, resource and retention checks still apply. Wrong tenant/type or inaccessible identities return no results. Resident lookup uses exact bank/user filters. Location/resident results retain authorized parent banks. No activation decoding or activation quota is used. HTTP errors and other text searches are unchanged.
GET /api/v1/search/users?q=166.2000-1100
SearchUsers supports kidOnly=true for direct resident Kid lookup even when ordinary resident search is disabled. Non-Kid input returns no items without business storage. Existing permissions apply; the toggle stays unchanged.
GET /api/v1/search/users?q=166.2000-1100&kidOnly=true
Kid search also accepts bank, location and resident identities without tenant: 2000, 2000.4 and 2000-1100. Kombine.Flex.Kid parses the identity; a missing tenant is filled exclusively from trusted API site configuration. An explicit tenant is preserved and checked. kidOnly=true also accepts tenant-relative resident IDs. Authorization is unchanged.
GetLocationUnits retains HTTP 403. After tab and resource checks, reason may be missing-location-read or missing-unit-read. Clients may show a localized explanation and use generic denial for unknown reasons. No location or unit data is read on denial.
{"status":403,"reason":"missing-unit-read"}
Account endpoints return HTTP 503 with code periods-timeout when the period query exceeds 10 seconds, storage-timeout for other SQL command timeouts, and storage-text-comparison for incompatible text collations. Show localized explanations and avoid repeated automatic retries. Raw SQL/database errors are not exposed. Date filters do not narrow the period lookup.
Account period choices use bank LogA (LocationId=0, UnitId=0, Period>0), up to 100 newest periods. Period zero remains selectable as the current period. Bank-wide access needs no Log1 lookup for this list. Location-limited access additionally checks for an authorized Log1 posting in each offered period. Entries, totals and revision still use Log1 with unchanged authorization.
Resident balance batches
GetBankUserBalances: POST /api/v1/banks/{bankKid}/users/balances is read-only. Send 1–50 canonical resident KIDs from the same bank, as returned by GetBankUsers. Numeric IDs and readable aliases are rejected. Duplicates appear once, preserving first-requested order.
const response = await fetch(`${apiBase}/api/v1/banks/${bankKid}/users/balances`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ userKids: visibleResidents.map(resident => resident.kid) })
});
if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
const { items } = await response.json();
// Use item.balances: one balance per currency. Never add different currencies together.
Requires an active manager, eTab.Users2, User Read and a bank-wide resource grant. A location-only grant cannot expose a balance across all locations. Permissions are checked on every request. Deleted residents follow the manager's retention rule. Missing or hidden residents return not-found and null, never a fabricated zero.
Use balances, containing one row per currency with currency, currentBalanceMinor, previousBalanceMinor, previousPeriod and previousPeriodIsProvisional. Amounts are signed 64-bit integers in the system's existing minor units (e.g. øre for DKK). No currency conversion occurs; never add different currencies together. Currency codes come from Log1 and are decoded, trimmed and uppercased. Missing/empty codes form a separate currency:null row; no currency is inferred. Rows are sorted by currency code.
The resident discount applies to DKK only. It is added once to the DKK row, which is also created when the resident only has postings in other currencies. An existing resident with no postings and no discount receives balances:[]. Missing/hidden residents also have an empty list, but their status is not-found.
Currency balances use period zero and FlexOrm's missing-settlement correction. Receipt documents are grouped by DocId and their settlement boundary is determined before separating the amounts by currency. Correction history applies tenant limits with positive manager overrides. The provisional-period discount cap uses only its DKK amount. These balances are not an instruction to charge.
Only selected residents are read in shared queries, reusing bank settings within the request, without a shared balance cache. At most two balance batches run per API process, waiting at most one second for capacity, with a 20-second database deadline. Correction history is capped at 50,000 lines per request. The portal resident list renders rows first, then loads current balances in batches of at most 10 residents, one batch at a time. Rows added by scrolling are handled the same way. Balances reload when filtering, sorting or navigating; the list does not poll balances. The API limit remains 50 residents per request.
400: invalid list/KID; 401: expired session; 403: insufficient permission; 413: oversized request. A 503 with storage-busy, storage-timeout, balance-batch-too-large or storage-unavailable returns no partial balances. Reduce oversized batches; back off before a bounded retry after busy/timeout responses. Display failure instead of zero. No database writes are performed.
Existing scalar fields on the resident result, request shape and operation ID remain unchanged for compatibility. The scalars retain the old calculation across currencies and must not be interpreted as one currency's balance; use balances. Both periods and every currency reuse the same Log1 query grouped by resident, period and currency, with no extra query per currency. At most 100 currency buckets are allowed per resident.
All currency rows use the resident's same latest positive period, even if a currency last appeared in an older period. No posting for that currency in the selected period means a balance of 0 for that period. No previous period remains null. Clients should not fall back to the old mixed amounts if the currency list is absent.
The previous period is the resident's highest positive period number, even if the bank has newer periods. A stored period's balance is its sum without adding the discount again. When no previous period exists, both its balance and period number are null, and the provisional flag is false. This includes residents with only period zero or no postings. A previous period with a zero balance instead returns 0 and its period number.
Delayed settlement produces a provisional previous balance using FlexOrm document grouping and capped discount. This also applies to cash banks, whose current balance stays unchanged. previousPeriodIsProvisional=true identifies a period computed only in memory; it has not been stored or settled. Its number is the bank's highest Log1 period plus one and must not be used as an existing settlement/download identifier. That number uses one indexed MAX lookup shared by the batch, only when a correction might be needed. A provisional period can have a zero balance.
GetBankUserBalances also returns latestPostingMs2000 (UTC milliseconds since 2000-01-01; zero means no Log1 postings) and hasActiveSubscription (active card/SEPA subscription). Both fields are null for missing/hidden residents and share the balance permissions and snapshot. Active status follows Orders: CardSubscription or SepaSubscription, Flags > 0, CR2000 > 0 and ActionCode OK/AUTHORIZE. It is not proof of payment. On HTTP 503, show unavailable and retry; never assume inactive or zero. Included in all client variants 0.4.2 for this beta release.
Installers — GetInstallers
GET /api/v1/installers?includeActivationCode=true adds items[].activationCode (the user activation code) and additionally requires Installer Create. Default false omits codes; missing Create returns 403 missing-installers-create. Codes are generated for authorized, retention-visible rows only. Do not log or cache them; passwords are never returned. The activation-code column is display-only, not a supported sort.
GetUserCount — GET /api/v1/users/count returns {count, iconKid} for active residents visible under UserFinder1, User Read and the current tenant-bound bank/location scope. The approximate badge count is cached in memory for up to 24 hours per scope; restart or eviction causes a fresh count. Deleted residents and list filters are excluded. Authorization is checked on every request. 401/403 means access denied; 503 means counting failed—keep the badge unknown and retry later, never assume zero. Example: curl -fsS "$API/api/v1/users/count" -H "Authorization: Bearer $TOKEN".
Installers default to eIcon.worker when the stored icon is missing, empty or invalid. A valid explicitly stored icon is preserved.
GET /api/v1/installers requires an active manager, Installers1 (68), independent PermissionInstaller2.Read and a whole-tenant KID grant. Bank-only or location-only access is insufficient. Account, credential stamp, tab, rights and scope are checked on every page through the bounded manager snapshot. The icon can be changed with SetInstallerIcon as described below.
The response is {items: [...], nextCursor: ...}. Each item contains kid, name, icon, email, locations, tags, deleted, deletedAt, enabled, lastActiveAt. All object identifiers are canonical KIDs; derive the displayed numeric UserId from the installer KID. Location and tag entries contain kid and enum-name state. They retain NoAccess and locked/deleted/history states: they describe the installer, not the caller's permissions. Location names are not looked up.
The trusted site's A{TenantId:D4}.Log7 is read with BankId=TenantId and eUserId.Installeres..InstalleresLast (1–999). Manager authorization settings still use bank zero. No credentials are selected. Disabled installers are included. Deleted rows follow the caller's RetentionDays; invalid/future deletion values are hidden. Missing Deleted means zero. Missing/invalid Enabled is null. Alive uses the current krumb's MS2000, never Text; absent, nonpositive or unrepresentable timestamps are null. UTC timestamps should be displayed in the viewer's time zone. Reads do not update Alive.
pageSize: 1–250, default 50. Follow nextCursor until null, including empty pages. Cursors bind caller, tenant, filter, ordering (page size may change).
filter: literal Name/Email substring, case-insensitive, trimmed, maximum 128 characters without controls. Escape URL parameters. SQL wildcard characters are literal.
sort: identity (default), name, email, locations, tags, deleted, enabled, lastActive; direction: asc (default) or desc. Ordering applies before paging. Name/email use ordinal case-insensitive comparison. Locations/tags compare sorted numeric ID/state lists lexicographically; deleted compares timestamps; enabled compares null/false/true; activity is chronological. Identity breaks ties in the same direction.
Identity ascending uses bounded keyset reads. Other orderings use a tenant-local index of at most 999 entries, cached for 60 seconds, then fresh page details with retention reapplied. Null sorts first ascending. Changes are not a frozen snapshot; restart if a cursor anchor disappears.
let cursor = null;
do {
const query = new URLSearchParams({pageSize: '50', sort: 'name', direction: 'asc'});
if (cursor) query.set('cursor', cursor);
const response = await fetch(`${api}/api/v1/installers?${query}`, {
headers: {Authorization: `Bearer ${token}`}
});
if (!response.ok) throw new Error(`GetInstallers: ${response.status}`);
const page = await response.json();
renderInstallers(page.items);
cursor = page.nextCursor;
} while (cursor);
Errors: 400 invalid-page/invalid-filter/invalid-sort/invalid-cursor (correct input or restart); 401 (log in again); 403 missing-installers-tab/missing-installers-read/missing-tenant-access (request the missing access); 503 installers-unavailable (storage failure or 12-second deadline, retry manually). Responses are no-store. No partial sorted results, background retry loop or write operations.
Installer details and icon — GetInstaller / SetInstallerIcon
GET /api/v1/installers/{installerKid} requires the same read access as the directory. The response contains installer, canEditIcon, iconRevision and availableIcons. The KID must be a canonical Installer KID for the site's tenant, with BankId equal to TenantId and a UserId in the installer range. Missing or retention-hidden installers return 404.
The icon catalogue uses the same metadata as the manager picker: all eIcon entries with eIconSubject.Person. A valid current icon without Person metadata appears first, but cannot be assigned again. POST /api/v1/installers/{installerKid}/icon requires both Installer Read and Write, tab 68 and whole-tenant access. canEditIcon is only a display hint; the API rechecks current account, rights and retention access during the save.
const detailResponse = await fetch(`${api}/api/v1/installers/${installerKid}`, {
headers: {Authorization: `Bearer ${token}`}
});
if (!detailResponse.ok) throw new Error(`GetInstaller: ${detailResponse.status}`);
const detail = await detailResponse.json();
const response = await fetch(`${api}/api/v1/installers/${installerKid}/icon`, {
method: 'POST',
headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},
body: JSON.stringify({icon: selectedPersonIcon, expectedRevision: detail.iconRevision})
});
if (!response.ok) throw new Error(`SetInstallerIcon: ${response.status}`);
const confirmed = await response.json();
renderInstallerIcon(confirmed.icon); // Apply only after a successful response.
The save changes only eSetting.Icon and returns kid, icon, iconRevision, availableIcons. An unchanged icon produces no history row. Other fields, location access and tags cannot yet be edited here.
400: invalid-installer-kid or invalid-installer-icon. 401 requires a new login. 403: missing-installers-tab, missing-installers-read, missing-installers-write or missing-tenant-access. 404: installer-not-found. 409: installer-icon-conflict; reread before selecting again. 503: installer-icon-unavailable. Keep the previous selection on failure; reread after an uncertain network outcome, and never retry a write automatically.
Administrators — GetManagers
Directory items and GetManager now include operationPermissions and retentionDays for the displayed administrator. The seven categories Managers, Bank, Location, Unit, User, Installer and Service reflect current Log7 PermissionManagers2, PermissionBank2, PermissionLocation2, PermissionUnit2, PermissionUser2PermissionInstaller2 and PermissionService2. The resource, level, flags and six can* fields have the same format as GetCurrentManager. Flags are independent: Write does not imply Read; missing/empty settings default to Read; invalid settings return null flags and no operations. Combine these flags with account state, tabs and Kids.
retentionDays is the displayed administrator’s number of days for viewing otherwise authorized deleted records. Missing values default to 10 days; invalid or negative values become 0. Explicit 0 is preserved. It does not schedule physical deletion. The signed-in caller still uses their own permissions and RetentionDays to read the administrator; viewing an account never grants that account’s permissions. Values are read with the other details in the same Log7 query; the sorting index is unchanged. RetentionDays remains read-only; permission editing is documented below. Read status codes are unchanged. Keep using the canonical manager Kid in the curl example below.
GetManager: GET /api/v1/managers/{managerKid} reads one administrator with the fields and authorization requirements of a directory item, plus the availableTabs catalog described below. Use its canonical kid from the list. A different-site KID or one outside the manager range returns 400 invalid-manager-kid. An absent administrator or a record outside retention returns 404 manager-not-found; the next administrator is never substituted. Handle 401, 403 and 503 as for the list. At most one Log7 candidate is read; no database values change.
The portal uses this operation for workspace shortcuts under Administrators. Shortcuts are local to the browser session, tenant and signed-in manager. They never grant access; each visit is rechecked by the API. The administrator overview currently supports reading only.
filter searches the entire administrator list for a case-insensitive substring of Name or Email. Maximum 128 characters; surrounding whitespace is removed. An empty filter lists all permitted entries. Filtering happens before the page limit; %, _ and ! are literal characters. Start without a cursor when changing the filter, and keep the same filter on following pages. Control characters or excessive length return 400 invalid-filter.
Each item also includes organisation, enabled, deleted and deletedAt. Organisation is empty when absent. Enabled is true for 1, false for 0, and null for missing/invalid values. Deleted is true for a positive deletion timestamp; deletedAt is then that UTC timestamp, otherwise null. These fields alone do not prove the account can sign in. Display unknown Enabled as unknown, and continue to enforce the documented permissions. Deleted entries remain limited by the caller’s retention allowance.
GET /api/v1/managers?pageSize=50 reads current tenant Log7 settings, bank zero, in the inclusive eUserId.Managers–eUserId.ManagersLast range. Root, resident and service accounts are outside this range. Items contain kid, name, icon, email, resourceGrants and tabs. Passwords are never returned.
Requires an active manager session, eTab.Managers1 (28), PermissionManagers2 Read and an explicit tenant-wide Kid grant. Bank/location grants are insufficient. Write does not imply Read; absent permission values default to Read. Every page rechecks these requirements using the shared session snapshot, at most 60 seconds old.
let cursor = null;
do {
const url = new URL('/api/v1/managers', API_BASE);
url.searchParams.set('pageSize', '50');
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!response.ok) throw new Error(`GetManagers: ${response.status}`);
const page = await response.json();
renderAdministrators(page.items); // Use textContent, never HTML from data.
cursor = page.nextCursor;
} while (cursor);
For user interfaces, request each following page near the bottom of the list. pageSize is 1–250, default 50; pageSize may change between requests. The opaque nextCursor is bound to the tenant, caller, filter, sort and direction. Follow it even after a short or empty page, until null. Restart without a cursor when changing these parameters. Pages are not a frozen snapshot of concurrent changes; if the continuation identity is absent from a refreshed index, 400 invalid-cursor requires restarting the list.
lastActiveAt in both GetManagers and GetManager is the last recorded activity: MS2000 on the current eSetting.Alive Log7 row, BankId 0, converted from milliseconds since 2000-01-01 UTC. It uses the row timestamp, not its Text value or other profile changes. Missing, nonpositive or unrepresentable timestamps return null; display these as unknown. Display valid UTC timestamps in the viewer’s local time zone. Reading does not update Alive, and this value is not proof that the administrator is online. sort=lastActive orders chronologically; unknown times come first ascending and last descending. The sorting index may be up to 60 seconds old while page details are read afresh.
sort=identity|name|email|organisation|kids|deleted|enabled|lastActive and direction=asc|desc order the complete permitted result before paging. The API default remains identity/asc; the portal uses name/asc. Name, email and organisation use case-insensitive ordinal comparison. Kids compare numerically sorted bank/location scopes for the current site, irrespective of stored Log7 order; empty scopes come first. Enabled orders unknown, false, true; Deleted orders by deletion timestamp, with active records first. Manager identity breaks ties; desc reverses the entire order.
Selectable ordering uses a shared seven-setting Log7 index cached for at most 60 seconds per filter. Only the selected page’s details are fetched afresh; access and deletion retention are rechecked each time. Indexes allow at most 20,000 matching identities, within a 50,000-weighted-entry cache (minimum weight 100 per filter). Concurrent index reads are coalesced; data failures delay new index reads for five seconds. A cold cache may need an extra query; the overall request deadline is 12 seconds. Identity/asc retains one bounded query per page. There are no total counts or per-manager queries.
Disabled administrators remain listed. Deleted administrators follow the caller’s RetentionDays; malformed deletion settings are hidden. Site-relative Kids are resolved to the API site, and only valid grants for that site are returned. Tabs follow AttributeMetaSortOrder, then numeric identity. These are assigned scopes and pages, not proof of effective operation permissions.
400 (invalid-page/invalid-cursor/invalid-sort): restart with valid parameters. 401: sign in again. 403: missing-managers-tab, missing-managers-read or missing-tenant-access identifies the missing requirement; do not retry automatically. 503 (managers-unavailable): show an error and offer a bounded retry with the same cursor. 503 manager-directory-too-large: narrow the filter and restart; the API never silently truncates a sorted list. The listing operation only reads data. Permission editing is described below.
Service comes from eSetting.PermissionService2 (3017) in current bank-zero Log7 and is returned by GetCurrentManager, GetManagers and GetManager. It uses the same six independent flags and missing/invalid-value rules. It does not enable service login or grant rights in other categories. Update it through SetManagerPermission with resource Service; existing authorization, concurrency and error handling apply.
Edit administrator permissions
SetManagerPermission: POST /api/v1/managers/{managerKid}/permissions/{resource}. Requires an active session, tab 28, a tenant-wide grant and both PermissionManagers2 Read and Write. Your own permissions are editable only if you are the sole active, non-deleted administrator with whole-tenant access. Every ordinary authorization requirement still applies. GetManager and directory items include isCurrentManager and canEditPermissions as display hints; the API rechecks the current account, credential stamp, tab and access inside the write transaction.
The exception checks other administrators’ Kids, Enabled and Deleted in the tenant’s bank-zero Log7. Only active accounts with an explicit whole-tenant grant count, regardless of their tabs or operation flags. Bank/location-only grants do not count. Directory filters, retention and cached counts never establish the exception. Writes lock the relevant records and ranges in the same serializable transaction as the change; a previously allowed display hint is insufficient. A read response containing your own record and Managers Write needs one extra bounded query for this hint. At most 20,000 other scope records are examined; if the absence of another administrator cannot be confirmed within this bound or the deadline, the request fails with 503.
Read GetManager first. Choose resource: Managers, Installer, Service, Bank, Location, Unit or User. Send one flag (Read=1, Write=2, Create=4, Delete=8, RenameExtrenatId=16, Rename=32), the desired enabled boolean and the category's displayed expectedFlags. The field is required, including explicit null for malformed stored rights. Only the selected bit changes; Write does not imply Read.
200 returns the saved category. Update the checkbox only after a valid OK response. Storage follows theme changes: append bank-zero Log7 history with the caller as actor and verify the current Log7 trigger before commit. Unchanged values append nothing. Tenant comes exclusively from server configuration. The target's local session cache is invalidated; other API instances may retain a read snapshot for up to 60 seconds. Permission writes always use fresh authorization. Disabled targets may be edited; deleted targets follow the caller's RetentionDays.
400: invalid KID, category, flag or input.
401: sign in again.
403: own-manager-permissions when another active administrator also has whole-tenant access, or missing tab, Read, Write or tenant-wide access. Refresh the card; do not retry automatically.
404 manager-not-found: absent or retention-hidden target.
409 permission-conflict: reload and review current permissions before trying again.
503 manager-permissions-unavailable or timeout: retain the displayed checkbox and show an error. A lost response may follow a committed write; reread before manually retrying. Never retry automatically.
A configured write connection and transactional Log7 tables are required. Only the seven Permission*2 settings can be edited here; names, Kids, tabs and other administrator fields are outside this operation.
Combine role (unreleased)
SetManagerPermissionRole — POST /api/v1/managers/{managerKid}/permission-role. The combine preset grants all six flags (63) in all nine permission categories, selects every current available tab, and adds the site Tenant KID for all banks in one atomic Log7 transaction. Send expectedTabsRevision and expectedKidsRevision from GetManager along with all nine expectedFlags. Existing active-account, Managers1, whole-tenant, Managers/Tabs/Kids Read+Write and own-edit checks still apply. Narrower site grants are replaced; foreign stored entries are preserved without granting access to other tenants. Future tabs are not selected automatically. The response adds resourceGrants, kidsRevision and canEditKids; validate the full permissions, tabs and grants response before updating the display. Missing revisions return 400 invalid-permission-role; stale values return 409 permission-conflict, tabs-conflict or kids-conflict; malformed storage returns invalid-stored-tabs or invalid-stored-kids. Nothing is partly saved. On 409 reread and review; on 503 or an uncertain response reread before a manual retry. No automatic retries. Account state and profile fields remain unchanged.
The operation permission matrix now has nine categories: Managers, Bank, Location, Unit, User, Installer, Service, Tabs and Kids. Role requests to SetManagerPermissionRole must send all nine expectedFlags, including the last displayed Tabs and Kids masks (explicit null for malformed values). Missing stored values default to Read (1); invalid values grant nothing. Operator, technical-support and tenant-accounting set all nine masks to 63; accounting and caretaker set Tabs/Kids to Read (1). SetManagerTab and SetManagerKid now additionally require the caller’s respective Tabs or Kids Read and Write, including transactional reauthorization. Every role replacement requires both Tabs and Kids Read/Write, rechecked inside the transaction, because all presets replace both permission masks. Accounting and combine replace tab assignments; other presets preserve them. Existing manager, tenant, own-edit and tab checks still apply. A denial returns 403 missing-tabs-read, missing-tabs-write, missing-kids-read or missing-kids-write. Respect canEditTabs and canEditKids independently of canEditPermissions/canEdit. These masks control editing assignments, not the caller’s existing navigation or KID scope; Read does not hide directory metadata. Create/Delete/Rename flags remain independent stored values and do not replace Write in assignment operations. No existing database values are migrated. This change is unreleased; update consumers before deployment.
The tenant-installer preset ({Tenant} - Installer) grants all six permission flags (63) in all nine categories, matching technical-support. SetManagerPermissionRole requires all nine expectedFlags and preserves existing tab selections and KID grants. Unreleased addition.
The new presets technical-support and tenant-accounting currently grant all six flags (63) in all nine categories and preserve tabs, matching operator. Both SetManagerPermissionRole accept these values with all nine expectedFlags. Unreleased addition.
Send all nine categories in expectedFlags, including Service. Missing or extra categories return 400 invalid-permission-role without writes.
Administrator role presets
SetManagerPermissionRole applies a complete preset in one call: POST /api/v1/managers/{managerKid}/permission-role. It uses exactly the same active-account, Managers1, Managers Read+Write, whole-tenant and own-card exception checks as SetManagerPermission. The API defines the masks; clients send the role identity, current expected masks and the required tab revision.
role
Bank
Location, Unit, User
Managers, Installer, Service
accounting (Regnskab)
Read (1)
Read (1)
None (0)
caretaker (Varmemester)
Read (1)
Read + Write (3)
None (0)
operator (Operator)
All six flags (63)
All six flags (63)
All six flags (63)
For accounting, also send the latest tabsRevision from GetManager as expectedTabsRevision. Existing clients must supply this new property when applying accounting. Missing or invalid revisions return 400 invalid-permission-role. The transaction validates both permissions and tabs before any write. A stale tab revision returns 409 tabs-conflict; malformed stored tabs return 409 invalid-stored-tabs. No part of the role is saved on either conflict. Read again and review before retrying. Set TABS_REVISION below to the revision you read.
Read GetManager first and copy all nine operationPermissions[].flags into expectedFlags, keyed by resource. Include explicit null for an invalid stored mask. The example assumes every currently displayed mask is 1:
The operation replaces all nine 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 nine 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 54 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 except combine preserve tabs. Kids, account status and profile fields stay unchanged. Roles are one-time presets, not stored memberships or automatic ongoing rules.
400 invalid-permission-role means an unknown role or missing, extra or invalid expected masks; malformed tenant/KID uses invalid-manager-kid. 401 requires login; 403 uses the same access codes as individual changes, including own-manager-permissions; 404 is manager-not-found. A mismatch anywhere returns 409 permission-conflict with no partial save: reload and review. On 503 manager-permissions-unavailable or an uncertain timeout, keep the previous display and reread before manually retrying. Never issue automatic retries or a sequence of individual bit calls as a replacement for the atomic preset. The existing 12-second deadline and sole-manager scan limit apply.
Edit administrator tabs
GetManager additionally returns availableTabs: every recognized numeric eTab except None and Length, ordered by AttributeMetaSortOrder then ID, with aliases deduplicated. Each item contains id, enum-derived name and iconKid. This catalog includes pages not yet implemented. Mark entries whose IDs appear in tabs. The directory omits the catalog; both reads return canEditTabs and the opaque tabsRevision.
SetManagerTab: POST /api/v1/managers/{managerKid}/tabs/{tabId}. Requires an active account, Managers1 (28), independent Managers Read and Write, and a whole-tenant grant. Own edits use the same sole active tenant-wide manager exception as permission changes. Display hints never authorize a write: the API rechecks the locked current account, tab, scope and permissions inside the transaction.
Read the target first, copy its exact tabsRevision, and choose an ID from availableTabs. The example grants tab 8:
TABS_REVISION='copy tabsRevision from GetManager'
curl -X POST "$API_BASE/api/v1/managers/$MANAGER_KID/tabs/8" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data "{\"enabled\":true,\"expectedRevision\":\"$TABS_REVISION\"}"
200 returns the saved tabs, next tabsRevision, canEditTabs and canEditPermissions. Update selection only after validating the successful response and use its revision for the next click. Removing your own Managers1 grant returns both edit hints false: lock all editor controls. Tab access is only one requirement; it does not grant Kids, account access or operation rights.
Only the requested tab changes. Unknown integer IDs and unrelated stored entries are preserved. The change uses the shared serializable bank-zero Log7 history transaction, caller attribution and verified current-table trigger; a no-op adds no history. Actor and target session caches are invalidated even on uncertain outcomes. Other instances may retain read snapshots for up to 60 seconds; every write rechecks current authorization. The 12-second deadline and sole-manager scan limit still apply.
400 invalid-manager-kid or invalid-tab-change: invalid/foreign KID, unavailable tab, missing boolean or invalid revision.
401: sign in again. 403: the same access codes as permission changes, including own-manager-permissions. 404: manager-not-found.
409 tabs-conflict: reread and review the latest selection. invalid-stored-tabs: stored JSON is not an integer array; no data was overwritten.
503 manager-tabs-unavailable, timeout or a lost response: retain the displayed selection and reread before a manual retry. Never retry writes automatically.
Administrator Kids
GetManager and GetManagers return resourceGrants, kidsRevision and canEditKids. Use SearchBanks and SearchLocations to find choices; their normal Read, scope and retention rules still apply. Residents and units are not valid resource grants.
SetManagerKid: POST /api/v1/managers/{managerKid}/kids. Requires an active manager, Managers1 (28), independent Managers Read and Write, and explicit whole-tenant access. Own edits require being the sole active tenant-wide manager. The API rechecks current Log7 snapshots inside the transaction.
A Bank KID grants the entire bank and replaces its individual location grants.
A Location KID grants only that exact location.
The site’s Tenant KID means All banks and replaces individual bank/location selections. Removing it later does not restore those selections.
enabled: false removes only the exact grant. Already covered additions are no-ops.
A confirmed 200 response returns the complete resourceGrants, next kidsRevision and canEditKids. Update selections only after confirmation. Removing your own tenant grant returns canEditKids=false; lock the entire editor. KIDs must be canonical and belong to the site tenant; clients cannot select a tenant. Added banks/locations must exist as exact Settings scopes in Log24. Removing stale grants is allowed.
Only Kids change through the existing audited bank-zero Log7 transaction. Tabs and other permissions are preserved. Stored site-relative grants resolve to this site; foreign stored entries are preserved without granting access here. Growth is limited to 1,000 Kids; removals and broader bank replacements remain allowed. Session and directory caches are invalidated; read caches on other API instances may live for up to 60 seconds. Every write reauthorizes.
400 invalid-manager-kid/invalid-kid-change: invalid/foreign identity, unsupported scope or missing boolean/revision.
401: sign in. 403: normal permission codes, including own-manager-permissions. 404 manager-not-found includes retention-hidden managers; resource-not-found means the selected bank/location does not exist.
409 kids-conflict: reread and review. invalid-stored-kids: invalid stored grants were not overwritten. kids-limit: too many grants.
503 manager-kids-unavailable, timeout or a lost response: keep the display, reread before a manual retry and never retry automatically. Writes have a 12-second deadline.
Deletion retention during a write and the confirmed canEditProfile hint use the same database clock as the deletion timestamp. An authorized caller can immediately restore another manager within their existing retention window, even when the API and database clocks differ slightly. Restore sends Deleted=false with the latest profileRevision; update the toggle only after a valid 200 response.
Edit administrator details
SetManagerProfileField: POST /api/v1/managers/{managerKid}/profile/{field}. Requires an active manager, Managers1 (28), Managers Read and Write and whole-tenant access. Own edits use the same sole active tenant-wide manager exception as permissions and tabs. These seven fields use Managers Write, without additional Delete or Rename flags. Every write rechecks the locked current authorization snapshot and target visibility.
First read GetManager. Its canEditProfile is a display hint; copy profileRevision exactly. Use the case-sensitive field names below:
field
JSON value
Stored meaning
Name / Organisation
String, up to 200 characters, no controls
Unicode/whitespace preserved; empty clears the field.
Enabled
true / false
1 / 0
Deleted
true / false
Server deletion time in MS2000 / 0
RetentionDays
Integer 0–2147483647
Days of visibility for otherwise authorized deleted records.
Icon
Exact eIcon name as a string
Only icons with eIconSubject.Person metadata may be newly assigned.
Email
One nonempty email address, up to 254 characters
Login address. No whitespace, controls or display-name syntax. Case is preserved.
Email format is validated before any database access: a plain ASCII internet address with at most 64 characters before @ and a fully qualified domain such as example.com. Domain labels contain 1–63 letters, digits or hyphens, with no leading/trailing hyphen. International domains use punycode. Empty addresses, name@company, repeated/leading/trailing dots, whitespace, quoted local parts and address literals return 400 invalid-profile-change. The stored address and revision remain unchanged. The portal shows a field error without sending a save request; a corrected address saves on the next blur. Other profile edits preserve existing legacy email values. This checks format, not mailbox existence.
Email edits use the same authorization and revision checks. The response includes the confirmed email. The password and existing sessions remain unchanged. The address is not verified and no email or invitation is sent by this edit. If multiple accounts later match the same email and password at login, the active, non-deleted manager with the newest Alive krumb MS2000 is retained and the other matching passwords are cleared as described in the login section. Invitations use the separate InviteManager operation.
GetManager.availableIcons contains all Person icons in numeric enum order. Mark the current iconKid. If it is outside the Person catalog, it appears first for display only; it cannot be newly assigned. Missing/unsafe filenames display as user without rewriting storage. The paged directory omits the catalog. Successful saves return iconKid and updated availableIcons. Change the selection only after a valid 200 response; preserve it on failure. Icon edits share revision protection and authorization with the other profile fields.
For deletion send {"value":true,"expectedRevision":"..."} to the /profile/Deleted endpoint. To restore, send false with the latest revision. The client never sends a deletion timestamp. The database server sets milliseconds since 2000-01-01 UTC; this is not Unix time. Repeating true preserves the first deletion timestamp. Restoration stores 0, never a boolean string. Only the selected field changes.
200 returns field, the seven profile values, deletedMs2000 (0 or timestamp), deletedAt (UTC or null), a fresh profileRevision and canEditProfile. Validate the complete response before updating the UI. False editability locks profile, permissions, role and tab controls. It occurs when disabling/deleting yourself or when deleting another manager hides them under your RetentionDays. Restore requires a caller whose existing retention includes that deleted manager; the write never bypasses retention.
The shared serializable bank-zero Log7 writer appends only changed settings, attributes the caller and verifies the current-value trigger before committing. It does not write legacy tables or alter passwords. Session and directory caches on this instance are invalidated even after an uncertain outcome; other instances can retain read snapshots for up to 60 seconds. Disabled/deleted accounts lose access at their next fresh authorization check; writes always reauthorize immediately. The 12-second deadline and sole-manager scan limit apply.
400 invalid-manager-kid or invalid-profile-change: wrong identity, unknown field, invalid type/range or missing revision/value.
401: sign in. 403: normal permission/own-card denial. 404 manager-not-found: absent or outside caller retention.
409 profile-conflict: one of the seven raw profile fields changed; reread and review.
503 manager-profile-unavailable or timeout: keep the previous state and unsaved drafts. A lost response may follow commit, so reread before a manual retry; never automatically retry.
MCP: read and write with manager permissions (unreleased)
Available in this source branch; not yet deployed. Configure an MCP client for https://api.{tenant}.kombine.technology/mcp (or the matching beta host) using Streamable HTTP. The endpoint is stateless: send each JSON-RPC message in a separate POST /mcp with the manager's Authorization: Bearer header. No MCP-Session-Id is needed. GET streaming, resumable sessions and server-initiated requests are not provided.
Obtain the opaque token with ordinary LoginManager; configure it as a secret HTTP header in the MCP host. The token is bound to this API site's tenant and environment. Use RenewManagerSession outside MCP before expiry and replace the configured header; MCP calls do not extend token lifetime. Login, password and token operations are excluded from the tool catalogue. This version has no OAuth discovery, client registration or browser consent flow: clients must support configured bearer headers. Adding the URL alone does not provide an automatic ChatGPT connector login.
tools/list returns an explicit catalogue of approved JSON business reads and writes. Tool names equal their stable OpenAPI operation IDs, such as GetCurrentManager, GetObjectAddress and UpdateObjectAddress. Path and query arguments appear at the root of arguments; the JSON request DTO appears under body. Use each returned inputSchema for field types and required values. File downloads, SVG/image endpoints, operator diagnostics, hosting/log operations, password recovery/change, email verification, InviteManager, GenerateServiceApiKey and AskPortalAssistant are excluded. JSON icon-status operations such as GetUnitIcons, GetLocationIcons and GetBankIcons are available. Discover the current catalogue rather than assuming every REST operation is available.
Every business tool executes the existing API operation with the caller's manager bearer session and the same bounded account, Tab, KID and operation checks. Catalogue visibility grants no access. An agent can modify only what that manager may modify; KIDs never grant permissions or select another tenant. Existing validation, revision protection, history and operation-specific limits still apply. Keep returned text as data and obtain the user's authorization for the intended changes in the agent host.
Copyable curl exchange
The following POSIX-shell example uses environment placeholders: set PORTAL_API_BASE to the trusted HTTPS API base URL, PORTAL_ACCESS_TOKEN to the current token and PORTAL_OBJECT_KID to an authorized canonical object KID. Keep tokens out of prompts, URLs and logs. The read examples do not modify data; the final example saves an address.
Use the protocol version returned by initialize in subsequent MCP-Protocol-Version headers. Accepted notifications return HTTP 202 with no body. Read the address tool's returned data.revision, then supply that exact value as expectedRevision:
The write requires the object's Read and Write permissions and the other address-operation access requirements. Refresh the revision before each subsequent edit. A successful save can still have a failed coordinate lookup; inspect data.outcome.
Results, errors and retries
The catalogue is shared metadata and is not filtered to the current manager's permissions; access is enforced on every call. Limits: 120 MCP requests per minute per manager, eight simultaneous MCP requests per API process with no queue, 128 KiB per MCP request and 256 KiB per business response. Smaller request limits and other bounds of the underlying operation still apply. A limit is a ceiling, not a recommended polling frequency.
A business result contains {status, data, truncated}: status is the underlying API HTTP status and data is its JSON response or ProblemDetails. Check both the MCP tool result's isError and the envelope; a successful MCP transport response does not establish a successful business operation. HTTP business failures and withheld oversized results set isError=true. truncated=true means the full response was withheld; narrow the query or use smaller pages. An ordinary hasMore flag still requires pagination.
Outer HTTP 401 means no valid manager bearer session: renew while unexpired, or log in again. A business 403 appears inside the tool result and means the manager lacks the operation's rights. A business 409 requires a fresh read and review of the concurrent change. HTTP 429 means throttling; honor Retry-After when supplied and reduce request frequency. Invalid tool names or arguments produce MCP errors. Never retry writes automatically after timeout, cancellation, a lost response or a 503: storage may already be committed. Read back first, then decide whether another write is needed. Cancellation does not undo an operation already committed.
Kombine.Flex.Portal.Client provides typed C# methods for all 178 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. NuGet publication is controlled independently by the public SDK repository. This beta provides the reviewed 0.13.0 archive directly; use the local installation below if that version is not listed on NuGet.org.
Platforms and dependencies
Consumer platform
Client asset selected by NuGet
Dependencies
.NET Framework 4.7.2 / 4.8 / 4.8.1
.NET Standard 2.0
Microsoft System.Text.Json 10.0.12 and its Microsoft dependencies
.NET 8 / 9
.NET 8
No additional packages
.NET 10
.NET 10
No additional packages
Checks cover builds against Framework 4.7.2 and 4.8, running on installed Framework 4.8.1, plus execution on .NET 8, 9 and 10. An original 4.7.2 installation and older .NET Core/.NET versions have not been exercised. Microsoft recommends Framework 4.7.2 or later for .NET Standard. Customers without NuGet can still use the separate DLL packages for 2.0, 4.5 and CE/Mobile.
First select the tenant API URL, then enter e-mail and password. The client uses existing API operations; API functionality and permission enforcement are unchanged.
Installation
NuGet publication is controlled independently by the public SDK repository. This beta provides the reviewed 0.13.0 archive directly; use the local installation below if that version is not listed on NuGet.org.
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 · 151 API-operations
kombine-flex-portal-client is a standalone Python 3.11+ package using only the standard library. No .NET, NuGet or other Kombine packages are needed.
Installation
Install the supplied wheel file locally. The package is not yet published to PyPI.
First select the tenant API URL ending in /, then email and password. Create a new client when switching tenants. The API enforces account, Tab, KID and operation permissions for every call. See access rules and error codes.
# Python: tenant_api_url, email and password come from your login form.
from kombine_flex_portal import PortalClient, PortalApiError
with PortalClient(tenant_api_url) as api:
try:
api.login(email, password)
manager = api.get_current_manager()
for tab in manager.get("tabDetails") or []:
print(tab.get("id"), tab.get("name"))
except PortalApiError as error:
print(error.status, error.code)
login() retains the token in memory. The raw login_manager(body) call does not retain a session. clear_session() and close() forget the local token; the API does not yet provide refresh or token revocation. Public calls never send the token. The client does not retain the password.
Operations and data types
All 178 operations have snake_case methods, such as get_bank_user_balances and get_installers. Request and response models are TypedDict in kombine_flex_portal.models. JSON field names, KIDs, cursors and revisions are preserved. Int64 uses exact Python integers; ISO dates remain strings. Null is not zero.
with api.export_bank_users(bank_kid) as download:
with open("residents.csv", "wb") as target:
for chunk in download.iter_bytes():
target.write(chunk)
PortalApiError exposes status, code, headers and response. The response may contain personal data. Network failures use URLError/OSError/TimeoutError; invalid or oversized JSON raises PortalProtocolError. No automatic retries or pagination.
The client is synchronous. The default timeout=30 is a socket I/O timeout, not an overall deadline for a long download. JSON defaults to a 16 MiB limit. Use a worker thread in async/GUI applications. HTTPS uses normal certificate validation; redirects are refused.
API contract changelog · Review request/response changes before updating your integration.
@kombine/flex-portal-client is one ESM package containing JavaScript and TypeScript declarations. It works in Node.js 22+ and modern browsers with fetch, BigInt and Web Streams. There are no runtime dependencies or other Kombine packages.
Installation
Install the supplied tarball. The package is not yet published to npm. TypeScript is optional; compiled JavaScript is already included.
First select the tenant API URL ending in /, then email and password. Create a new client when switching tenants. The API enforces account, Tab, KID and operation permissions for every call. See access rules and error codes.
import { PortalClient, PortalApiError } from '@kombine/flex-portal-client';
const api = new PortalClient(tenantApiUrl);
try {
await api.login(email, password);
const manager = await api.getCurrentManager();
for (const tab of manager.tabDetails ?? []) console.log(tab.id, tab.name);
} catch (error) {
if (error instanceof PortalApiError) console.error(error.status, error.code);
else console.error('The API request could not be completed.');
} finally { api.clearSession(); }
login() retains the token in memory. The raw loginManager(body) call does not retain a session. clearSession() and close() forget the local token; the API does not yet provide refresh or token revocation. Public calls never send the token. The client does not retain the password.
Operations and data types
All 178 operations have camelCase methods and exported TypeScript interfaces. Int64 fields use bigint, including expiresIn, so large balances are not rounded. Int32 uses number; dates remain ISO strings. KIDs, cursors, revisions and null are preserved.
Use value.toString() to display bigint. JavaScript JSON.stringify cannot directly handle bigint; explicitly choose a representation such as strings for your own JSON output. The client serializer sends int64 correctly as JSON numbers. Do not blindly convert large amounts to Number.
PortalApiError exposes status, code, headers and response; entire responses may contain personal data. Network/CORS failures use fetch errors, cancellation/timeouts normally use AbortError/TimeoutError, and invalid JSON raises PortalProtocolError. No automatic retries or pagination.
The default timeoutMs: 30_000 covers the entire response, including streaming. Each call accepts AbortSignal. JSON defaults to a 16 MiB limit; file downloads are streamed and must be closed. HTTPS uses normal certificate validation, and redirects are refused.
Browser / CORS
Use a bundler, or host the entire package dist directory on your webserver and import ./dist/index.js from a <script type="module">. The API Cors:AllowedOrigins must allow the page’s exact origin. Browsers can only read CORS-exposed response headers. Node.js does not require browser CORS.
Customers with legacy .NET Framework 2.0 applications can reference Kombine.Flex.Portal.Client.Net20.dll using Add Reference → Browse. The ZIP includes the DLL, XML IntelliSense documentation, source, Kombine.Flex.Portal.Client.2008.sln and a standalone test application. No NuGet or Kombine packages are needed; only mscorlib 2.0 and System 2.0. This targets .NET Framework 2.0, not .NET Standard 2.0.
Installation
Extract the ZIP and add the client DLL using Add Reference → Browse. Keep the supplied XML file beside the DLL for IntelliSense. NuGet is not required.
Login and tabs
Place using/Imports directives at the top of the file and the remaining code inside a method. Input variables (tenantApiUrl, credentials and KIDs) come from your application. The examples use the same DLL in both languages.
C#
using System;
using Kombine.Flex.Portal.Client.Net20;
using (PortalApiClient api = new PortalApiClient(new Uri(tenantApiUrl)))
{
ManagerSessionResponse session = api.Login(email, password);
ManagerProfileResponse manager = api.GetCurrentManager();
Console.WriteLine(manager.Name);
if (manager.TabDetails != null)
{
foreach (ManagerTabResponse tab in manager.TabDetails)
Console.WriteLine("{0}: {1}", tab.Id, tab.Name);
}
api.ClearSession();
}
VB.NET
Imports System
Imports Kombine.Flex.Portal.Client.Net20
Using api As New PortalApiClient(New Uri(tenantApiUrl))
Dim session As ManagerSessionResponse = api.Login(email, password)
Dim manager As ManagerProfileResponse = api.GetCurrentManager()
Console.WriteLine(manager.Name)
If manager.TabDetails IsNot Nothing Then
For Each tab As ManagerTabResponse In manager.TabDetails
Console.WriteLine("{0}: {1}", tab.Id, tab.Name)
Next
End If
api.ClearSession()
End Using
Select the tenant API URL first. All 178 public integration operations have synchronous methods without an Async suffix. Optional filters use OperationOptions classes, such as GetBankUsersOptions. Dates/timestamps are ISO 8601 strings preserving offsets; money in minor units uses nullable Int64. Keep KIDs/cursors/revisions unchanged. The library contains no database access or business rules.
API errors use PortalApiException with StatusCode, Code and Headers; follow the same 400/401/403/404/409/429/503 rules described above. Network/TLS/timeouts use WebException. There are no automatic retries. Calls block and should run on a background thread in desktop UIs. ClearSession forgets the local token; use RenewManagerSession explicitly for renewal; individual token revocation is unavailable.
HTTPS requirements: .NET 2.0-compatible code still needs a patched Windows/CLR 2.0 installation supporting TLS 1.2 and trusted certificates. The library does not change machine settings. Explicitly calling PortalApiClient.EnableTls12() selects TLS 1.2 for the entire host process and fails if unsupported. See Microsoft's TLS guidance. No fallback disables certificate validation or sends customer data over insecure HTTP.
API contract changelog · Review request/response changes before updating your integration.
Kombine.Flex.Portal.Client.Net45.dll exposes the same 178 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 178 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.
Source package 0.2.1 includes 178 operations from the bundled contract, examples, synthetic tests and English, Danish and Spanish READMEs. Requires Dart 3.6 or later (below 4.0). Extract beside your application and add this path dependency:
Use the tenant API URL and manager credentials supplied by your application. Object models use wire-map fields; int64 values use BigInt. HTTPS and normal API permissions apply. No automatic retries. Flutter device execution has not been tested. This package is not published to pub.dev.
64-bit PHP 8.2+, ext-curl and ext-json. No additional PHP libraries. Version 0.13.0 is included in the API downloads. It is not published to Packagist.
Version 0.3.1 synchronizes GetBankUserBalances documentation with its 20-second database deadline. Request and response fields are unchanged. Allow extra time for transport and authorization; HTTP 503 still returns no partial balances. Version 0.2.5 adds optional latestPostingMs2000 and hasActiveSubscription fields to GetBankUserBalances. The posting time is a 64-bit UTC millisecond count since 2000-01-01; zero means no postings. Null or an absent field means unknown, and missing or hidden residents return null. Subscription status is not payment confirmation. Keep existing balance handling and permissions; see /docs#user-balances. Version 0.2.5 adds GetLocationOpeningHours and GetLocationBookingRules. Both require Location Read, Unit Read and the authorized location scope. Reservation rules contain plain text plus ordered parts with text/isValue for optional value emphasis; never render these strings as HTML. Keep text as the fallback for older responses. See /docs#location-opening-hours and /docs#location-booking-rules for permissions, examples and limits. Version 0.2.5 adds GetUserReceipts and GetHostingMetrics for API releases that expose these operations. Load receipts on demand, starting at offset 0. Continue with nextOffset and the same revision; on HTTP 409 (receipts-changed), discard earlier pages and restart at offset 0. Keep currencies separate and minor-unit amounts as 64-bit integers. See /docs#user-receipts and /docs#hosting for permissions and limits.
Without Composer: extract to flex-portal-client/ and require its autoload.php instead. Keep the complete src/ folder. The ZIP includes English, Danish and Spanish READMEs, operation and model references, and the public OpenAPI snapshot.
Use a manager email and password for the tenant Portal API. Supply tenantUrl and the login variables from protected application configuration or a login form. The API URL must end with /. Keep the bearer on the PHP server.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Kombine\Flex\Portal\PortalClient;
use Kombine\Flex\Portal\ApiException;
use Kombine\Flex\Portal\ProtocolException;
use Kombine\Flex\Portal\TransportException;
$api = new PortalClient($tenantUrl, timeout: 30);
try {
$session = $api->login($email, $password);
$result = $api->getCurrentManager();
} catch (ApiException $error) {
$status = $error->status;
$code = $error->apiCode;
$retryAfter = $error->headers['retry-after'] ?? null;
// Handle according to the table below; do not blindly repeat writes.
} catch (TransportException | ProtocolException $error) {
// Timeout, network failure, malformed data or response limit.
// A write may already have completed: check state before repeating it.
} finally {
$api->close();
}
renew() explicitly calls RenewManagerSession and stores the replacement token. Raw renewManagerSession() only returns the response. Renew while the bearer is unexpired, based on user activity; after expiry or revocation, log in again. There is no separate refresh token.
Every business call still requires the manager’s account state, permitted Tab, KID scope and operation permission. A KID itself grants no access.
Paging is explicit: keep the returned cursor and continue until it is absent, including after an empty page. Keep revision tokens for writes. Use a temporary file for downloads and rename it only after success.
$page = $api->getBankUsers($bankKid, ['pageSize' => 25, 'sort' => 'number']);
$balances = $api->getBankUserBalances($bankKid, ['userKids' => $userKids]);
$receipts = $api->getUserReceipts($userKid, ['offset' => 0]);
// Request the next page only when needed, using nextOffset and the same revision.
$stream = fopen($temporaryPath, 'w+b');
try {
$download = $api->exportBankUsers($bankKid, $stream);
} finally {
fclose($stream);
}
rename($temporaryPath, $completedPath);
ApiException exposes status, apiCode and lowercase headers. 400: correct input; 401: sign in; 403: check permissions; 404/409: reload and resolve conflicts; 429: respect Retry-After; 5xx: handle temporary failure. TransportException and ProtocolException mean network/deadline or invalid/oversized data. A failed write may have completed: check state before retrying. Never log credentials, tokens or complete payloads.
Responses use associative arrays; lists use indexed arrays. Use native 64-bit int for integer fields; preserve missing/null values. The client verifies TLS, refuses redirects, omits tokens on anonymous calls and clears the session on HTTP 401. Default limits: 30 seconds total, 16 MiB JSON, 64 KiB headers, 1 GiB per download. Downloads write to a caller-owned stream; discard partial files on failure. No automatic retries, paging, renewal or acknowledgements. Server-side PHP does not need browser CORS.
Unit directory (Units1)
Running units (locally implemented, unreleased):GET /api/v1/units?runningOnly=true&fields=progress&sort=progress&direction=asc filters before sorting and pagination. A running unit has valid positive Log24 Started/Done timestamps with start <= calculation time < expected end. Missing, invalid, future-start and completed timing is excluded. Default is false; selecting progress is optional. Combine with enabledOnly, search and authorized location/terminal scope. No additional device-state or machine-type filter is applied. Calculation time stays fixed across nextCursor pages for at most 15 minutes; refresh the first page for current running units. Repeat runningOnly with every cursor; changing it returns 400 invalid-cursor. GetTerminals rejects runningOnly=true with 400 invalid-filter. Existing Units1, Bank/Location/Unit Read, resource scope, retention and 401/403/503 behavior remain unchanged.
Optional progress (locally implemented, unreleased):GET /api/v1/units?fields=progress adds items[].progress: {status,percent,remainingSeconds,calculatedAtUtc}, using exactly the overview-card calculation. It reads lifecycle inputs in one bounded Log24 query for the authorized page and calculates at request time. The percentage uses only the start and expected end times, clamped to 0–100. Missing or invalid times give null percent. Without selection, progress is null. GetTerminals does not populate it. Use sort=progress with direction=asc/desc for numeric sorting before pagination (GetUnits only). Unknown values sort first ascending and last descending. The calculation time is fixed across each cursor chain for at most 15 minutes, so time passing alone cannot move units between pages; device updates can still move rows. Permissions and retention are rechecked at the current time. Progress cannot be searched; preserve fields with nextCursor. Existing Units1, Bank/Location/Unit Read, scope and RetentionDays checks and 400/401/403/503 responses apply. The value is a snapshot, not a live update.
GetUnits additionally accepts terminalKid: GET /api/v1/units?terminalKid={controllerKid}. The parameter is a canonical Controller KID with MainId (UnitId=0). Returns the first unit and child units mapped by same-location Alive.MainId to that controller. The parent must be a retention-visible, authorized Log24 unit; contradictory mappings, chains and cycles do not qualify. A missing parent Alive row is allowed, but missing child mappings never infer membership. Existing Units1 and Bank/Location/Unit Read checks apply. Filtering precedes pagination. Repeat terminalKid with every cursor; changed options return 400 invalid-cursor. Invalid/wrong-tenant KIDs and terminalKid on GetTerminals return 400 invalid-terminal. Omit it for the existing directory. Locally implemented, unreleased.
Optional locationKid on GetTerminals and GetUnits restricts the directory to one canonical location KID in the site tenant. It intersects existing grants before search, sorting and pagination; authorization and terminal classification are unchanged. Example: GET /api/v1/terminals?locationKid={locationKid}&enabledOnly=true. Repeat it unchanged with each cursor; a changed scope returns 400 invalid-cursor. Invalid or wrong-tenant KIDs return 400 invalid-location. Omit it for the cross-location directory. Implemented locally, unreleased.
Unreleased: sort=kid orders GetUnits and GetTerminals numerically by bank, location and unit within the trusted tenant. Use direction=asc or desc; keep both options unchanged when following a cursor.
GetUnits — GET /api/v1/units
Requires an active manager with Units1 (50), Bank Read, Location Read, Unit Read and matching resource grants. Includes main and child units discovered in the trusted tenant’s Log24. Limited grants require an enabled parent location and retained bank/location/unit deletion states; site-wide grants may inspect all states. enabledOnly=true narrows the result to units with Enabled exactly 1.
Use fields=locationName,bankName,unitType,washDocId,outOfOrder for optional columns. Core canonical kid, locationKid, bankKid, names, iconKids and enabled/deleted/deletedAt are always returned. Optional unit values are null unless selected and empty when absent. Type comes only from original packed UnitType using (value >> 1) & 255; UnitType2 is ignored, including as a fallback. includeCoordinates=true returns parent-location latitude/longitude in decimal degrees; missing/invalid coordinates are null.
filter searches stored names, unit numbers/types, WashDocId and OutOfOrder, or an exact canonical/site-relative bank, location or unit KID. sort accepts kid, name, bankName, locationName, unitId, unitType, washDocId or outOfOrder; direction=asc/desc. Unit number/type and WashDocId sort numerically; text uses utf8mb4_general_ci. Display names can be localized; search/order use stored names.
pageSize is 1–250 (default 50). Repeat the same options and bearer session with nextCursor until null. Cursors expire after 15 minutes and bind tenant, manager, grants, retention and query options. Restart without a cursor when changing options. Concurrent edits can move rows.
400 invalid-page/filter/sort/fields/cursor: correct input or restart paging. 401: sign in again. 403 missing-units-tab/missing-bank-read/missing-location-read/missing-unit-read/missing-resource-access: review permissions. 503 units-unavailable: retry manually. No-store. Included in this host release and clients 0.13.0.
GET /api/v1/units?pageSize=50&fields=locationName,bankName,unitType&enabledOnly=true&sort=name&direction=asc
Authorization: Bearer <session-token>
Active unit count
GetActiveUnitCount — GET /api/v1/units/active-count returns {count,iconKid} for the Units1 navigation badge. Reuse the manager bearer session. It requires the same active manager, Units1 (50), Bank Read, Location Read, Unit Read and matching site/bank/location grants as GetUnits. Counts distinct Log24-discovered main and child units with Enabled exactly 1 and Deleted zero or absent; malformed deletion values and deleted units are excluded, including for site-wide grants. Limited grants still require an enabled parent location and bank/location deletion states visible under RetentionDays; site-wide grants bypass only those parent-state restrictions. The trusted site fixes the tenant.
The count is independent of search, selected columns, sorting and paging; no directory query parameters are needed. iconKid is the complete icon identity with its count badge: render it unchanged through the icon API instead of composing a badge identity. It grants no access. Counts are read afresh; permission snapshots may be up to 60 seconds old. Responses are no-store. 401: sign in again. 403 missing-units-tab, missing-bank-read, missing-location-read, missing-unit-read or missing-resource-access: review permissions. 503 units-unavailable means storage is unavailable or the 12-second deadline expired: allow manual retry and never display the failure as zero. The portal loads the count in the background as it does for Locations, with the full count in the icon badge and localized help. Implemented locally, not yet released; generated clients and download packages will be synchronized at beta deployment.
Terminal directory (unreleased):GetTerminals (GET /api/v1/terminals) returns name from eState.ComputerName, not eSetting.Name. Name filtering/sorting follows ComputerName; missing names are empty. Request additional decoded string columns with ?fields=versionMinor,bootReason,booted,firmware,storageCardSerialNumber,page,backLight. Booted uses eSetting.Booted; the others use eState. Unselected fields are null; selected missing values are empty. These fields can also be used as sort values (text ordering). Keep fields and sort unchanged when following nextCursor. Terminals1 and Bank/Location/Unit Read plus bounded resource grants remain required; invalid fields/sorts return 400, missing permissions 403. This does not change GetUnits.
Each row's iconKid uses the fixed terminal icon, independent of eSetting.Icon, with UnitId in its Text field. Render it unchanged; do not replace it with the lazy unit-icon response.
GetTerminals — GET /api/v1/terminals returns the same UnitDirectoryResponse shape as GetUnits: {items,nextCursor,hasAllBanksAccess,fields}. Requires an active manager with Terminals1 (66), Bank Read, Location Read, Unit Read and matching site/bank/location grants. Terminals1 is checked independently; Units1 is not required. Every page rechecks permissions and RetentionDays. Limited grants require an enabled parent location and retained bank/location/unit deletion states; an explicit site-wide grant may inspect all states.
Only Log24-discovered units whose UnitId equals a positive Alive.MainId at the same bank and location are returned. Several Alive rows may refer to one main terminal; it appears once. An Alive child row can identify its main terminal but is not itself a terminal result. Missing/zero MainId, orphan Alive identities and unauthorized units are excluded. This is a read-only classification from the trusted tenant's Alive table; it does not discover extra equipment, report online status or grant access. GetUnits continues to include main and child units.
Use the same query options as GetUnits: fields=locationName,bankName,unitType,washDocId,outOfOrder, filter, sort=kid|name|bankName|locationName|unitId|unitType|washDocId|outOfOrder, direction=asc|desc, enabledOnly, includeCoordinates, pageSize and cursor. Core canonical KIDs, names, iconKids and enabled/deleted/deletedAt remain populated; unselected optional unit fields are null and selected missing values are empty. Type comes only from original packed UnitType using (value >> 1) & 255; UnitType2 is ignored, including as a fallback. A filter of at most 128 characters searches stored names, unit numbers/types, WashDocId and OutOfOrder or an exact canonical/site-relative bank, location or unit KID. Display names may be localized; search/order use stored values. Numeric unit/type/WashDocId ordering and text comparison match GetUnits. enabledOnly=true requires Enabled exactly 1. includeCoordinates=true returns the parent location's nullable decimal-degree coordinates for maps; follow every page to collect the complete authorized map.
pageSize is 1–250, default 50. Reuse the manager bearer session and repeat unchanged query options except pageSize with nextCursor as cursor until it is null. Keyset cursors expire after 15 minutes and bind the directory, tenant, manager, grants, retention and options. Restart without a cursor after changing them; a GetUnits cursor cannot be used for GetTerminals. Concurrent edits or Alive changes may move rows.
Replace the HTTPS origin with your tenant's Portal API and reuse a valid manager token:
400 invalid-page, invalid-filter, invalid-sort, invalid-fields or invalid-cursor: correct input or restart paging. 401: sign in again. 403 missing-terminals-tab, missing-bank-read, missing-location-read, missing-unit-read or missing-resource-access: review permissions. 503 terminals-unavailable: show the failure and allow manual retry; do not treat it as an empty directory. Responses are no-store. Implemented locally, not yet released; generated clients and download packages will be synchronized at beta deployment.
Active terminal count
GetActiveTerminalCount — GET /api/v1/terminals/active-count returns {count,iconKid} for the Terminals1 navigation badge. It requires the same active manager, Terminals1 (66), Bank Read, Location Read, Unit Read and matching site/bank/location grants as GetTerminals; Units1 is not required. Active-state and parent-visibility rules match GetActiveUnitCount. Only authorized Log24-discovered units whose UnitId matches a positive tenant-bound Alive.MainId at the same bank and location contribute. Each terminal identity counts once, even when multiple child Alive rows identify it; those children are not extra terminals.
Reuse the manager bearer session; no search, column, sort or paging parameters are needed. Render the returned iconKid unchanged through the icon API; it grants no access. The count is read afresh with no-store responses, independently of directory filters. 401: sign in again. 403 missing-terminals-tab, missing-bank-read, missing-location-read, missing-unit-read or missing-resource-access: review permissions. 503 terminals-unavailable means storage is unavailable or the 12-second deadline expired: allow manual retry and never display the failure as zero. The portal loads the badge in the background as it does for Locations. Implemented locally, not yet released; generated clients and download packages will be synchronized at beta deployment.
More1 tools move from bank context to the tenant workspace
ConvertToolTime — POST /api/v1/tenant/tools/time and InspectToolKid — POST /api/v1/tenant/tools/kid replace POST /api/v1/banks/{bankKid}/tools/time and POST /api/v1/banks/{bankKid}/tools/kid. The old routes and required bankKid path parameter are removed. JSON request/response fields and operation IDs are unchanged. The trusted site selects the tenant; no tenant or bank selector is required. An active manager, More1 (29), Bank Read and at least one resource grant in the site's tenant remain required. Bank- and location-restricted grants qualify without selecting a context bank. The obsolete 400 invalid-bank error is removed; credential, permission, conversion and availability errors retain their meanings.
Migration: change the request URL to /api/v1/tenant/tools/time or /api/v1/tenant/tools/kid, remove the bank path parameter and reuse the existing manager bearer token and JSON body. Open More1 from the tenant workspace. KID inspection remains a syntax-only operation that grants no access to the inspected identity. Portal clients 0.13.0 and Dart 0.3.0 include the new signatures; rebuild callers without bankKid. Verified on all five beta apps on 2026-10-26 at commit aa51399c. Production is pending.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Unit document lists use one date and include earlier history
GetUnitDocuments — GET /api/v1/units/{unitKid}/documents: request parameters from/through are replaced by date, a UTC calendar date in yyyy-MM-dd format, defaulting to today. Previously, the default interval covered 24 hours and all searches were limited to 31 days. The list now includes matching document identities from 2000-01-01 through the entire selected day. Response from is the epoch and through is the date at 23:59:59.999Z; their types and the remaining page fields are unchanged. Supplying the removed request parameters returns 400 invalid-document-search. Bank-wide search and full-document measurement limits are unchanged.
Migration: send one UTC date and reset offset when it or sorting changes. Fetch pages lazily, preserving that date. For a total, call GetUnitDocumentsCount — GET /api/v1/units/{unitKid}/documents/count with the same date independently of page loading; it returns {count, from, through}. Both operations retain the same tenant, manager, WashDoc3, Read, scope and retention checks. Verified on beta 2026-10-23 at commit 3c2a88b; clients are synchronized. Production pending.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
WashDoc measurements and signals follow the original unit type
GetUnitDocumentGraph — GET /api/v1/documents/{documentKid}/graph: for Alliance dryers 19/29/30, a historical Level series previously appeared as water level with an unknown unit; it now has group=temperature2, Temperature2 label/localization and °C for bare numeric values. Its stable source name=Level and values are unchanged. LevelTarget/LevelUnfiltered use group=legacyLevel and unknown units. Bare Humidity on those three types uses %. Their signals now accept flags 0–127, interpret heat as bits 32|64, expose nullable airflow from bit 16, and return null inlet/outlet instead of interpreting dryer switches as water valves. Other types retain existing signal semantics. Bare Spin on QW washer types 15/16/17/25/31 no longer claims rpm: series/point units are null and values 0–3 use step=true. Explicit rotation-scale JSON keeps its recorded units.
The added presentation object contains trusted original unitType, kind and preferred groupOrder. Additional actual groups remain present. Migration: use each returned semantic group, label, unit and step flag instead of inferring measurement meaning from its source name. Treat null water signals as inapplicable/unknown, never off, and handle the new airflow signal. Apply preferred ordering only to groups present in the document and retain unlisted groups. PNG, SVG and public widget rendering share these type rules. Stored data, request parameters and authorization are unchanged. Verified on beta 2026-10-23 at commit 3c2a88b; clients are synchronized. Production pending.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Document tables and downloads omit columns without data
GetUnitDocumentTable — GET /api/v1/documents/{documentKid}/table, DownloadUnitDocumentCsv — GET /api/v1/documents/{documentKid}/table.csv, DownloadUnitDocumentXls — GET /api/v1/documents/{documentKid}/table.xls and GetUnitDocumentHtml — GET /api/v1/documents/{documentKid}/table.html: selected fields previously produced columns even when they contained no values. Now columns omits a field when every cell in the complete authorized document has no numeric value and null, empty or whitespace-only text. Numeric zero, false and nonempty text remain data. The same positions are removed from each rows[].cells array and download headers/cells. Remaining column order, rows, timestamps and values are preserved; no populated field is dropped because its data appears late. A document without populated fields returns empty columns and cell arrays, while downloads retain their timestamp columns.
GetUnitDocumentGraph — GET /api/v1/documents/{documentKid}/graph shares the filtered data: a signal column containing only null/empty values now contributes no signals entries. Absence remains unknown, never false/off. Stored null transitions in a column with actual data are retained.
Migration: construct column mappings from the returned kind/name metadata on every response; do not assume every requested field is present or reuse positional indexes from another document. Treat an omitted field as having no values in this document. Authorization, field-selection limits and storage are unchanged. Verified on beta 2026-10-23 at commit 3c2a88b; clients are synchronized. Production pending.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Machine types use only the original packed UnitType
GetLocationUnits — GET /api/v1/locations/{locationKid}/units, GetUnitOverview — GET /api/v1/units/{unitKid}, GetUnitGroup — GET /api/v1/units/{unitKid}/groups/{kind}/{group}, GetControllerOverview — GET /api/v1/controllers/{controllerKid}, GetControllerGroup — GET /api/v1/controllers/{controllerKid}/groups/{kind}/{group}, GetUnits — GET /api/v1/units, GetTerminals — GET /api/v1/terminals, GetTenantStatus — GET /api/v1/tenant/status and GetTenantStatusPage — GET /api/v1/tenant/status/page: type interpretation now uses only original eSetting.UnitType, decoded as (value >> 1) & 255. Previously a present direct UnitType2 took precedence. UnitType2 is now ignored and never supplies a fallback. Missing, malformed or out-of-Int64-range UnitType remains unknown; no default type is invented. Stored settings are unchanged.
Affected results include unitType, unitTypeName, unitTypeSource (UnitType when present, null when absent), available setting/state groups, directory type search/filter/sort, SMS exclusion in AutoOutOfOrder, and type-derived progress/widget presentation. Field types and authorization are unchanged, but values and available groups can change. Migration: discard cached type/group metadata, use the returned original type rather than interpreting UnitType2, restart directory cursors and refresh field revisions before editing. Handle unknown types and groups that are no longer available. This supersedes the source-precedence rule recorded in the 2026-10-12 entry. Verified on beta 2026-10-23 at commit 3c2a88b; clients are synchronized. Production pending.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Unit and terminal cursor options support adaptive page sizes
GetUnits — GET /api/v1/units and GetTerminals — GET /api/v1/terminals: the opaque cursor option binding no longer includes pageSize. Previously, pageSize was fixed by the cursor and limited to 100; it now accepts 1–250 and may change between pages. Cursors issued before this update fail with HTTP 400 invalid-cursor. Migration: discard an old cursor and restart the same filtered query without it. Keep all other query options unchanged, treat cursors as opaque, and continue until nextCursor is null. Tenant, manager, scope, retention and operation checks remain enforced. Other private directory endpoints also accept pageSize 1–250 and permit size changes without changing their filter/scope binding. Response fields and UserBalance are unchanged.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Reservation cards include resident display details and require User Read
GetLocationReservations — GET /api/v1/locations/{locationKid}/reservations and GetLocationReservationHistory — GET /api/v1/locations/{locationKid}/reservation-history now require User Read alongside Bookings1, Location/Unit Read and existing resource/retention checks. Previously these operations returned occupancy/history without resident metadata or User Read. Authorized items add userNumber and userName strings from the latest resident bank Log7 settings; missing values are empty strings. Items also add nullable canonical userKid, used as the display fallback for a missing number (null when no resident is recorded). Existing time, unit and revision fields are unchanged. Migration: grant User Read to managers who may inspect these details, handle 403 for managers lacking that permission, and render the new strings as plain text. Do not treat failed reads as availability.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Invalid stored calendars are omitted
GetLocationBookingCalendars — GET /api/v1/locations/{locationKid}/booking-calendars: malformed stored calendars are now omitted from groups, alongside missing calendars. Previously they returned status: Invalid with empty days. Valid calendars on other authorized units remain visible. Migration: treat omitted units as having no valid configured calendar, not unrestricted booking availability; remove expectations of Invalid groups. Storage and authorization failures still return their documented errors.
Beta 2026-10-23 · 3c2a88b · clients synchronized · production pending
Booking calendars return opening-aware dates instead of repeating weeks
GetLocationBookingCalendars — GET /api/v1/locations/{locationKid}/booking-calendars adds optional from (ISO date) and days (1–31, default 14). Invalid date ranges return 400. The response adds timeZone, from, days and per-day date. Previously group days were a seven-day recurring template with potentially negative starts and multi-day durations. They now represent the requested dates with slots clipped to midnight and effective opening hours; type 3 means unknown opening hours. Groups also distinguish different opening schedules.
Migration: render returned dates directly, fetch additional ranges when navigating, remove client-side weekly repetition and opening-hour merging, and display unknown periods as unavailable. This remains configuration rather than booking availability. The current Portal client packages include the dated contract. Verified on all five beta apps on 2026-10-23 at commit 3c2a88b; production pending.
Endpoints:GetCameraCaptures — GET /api/v1/camera and GetCameraCaptureContent — GET /api/v1/camera/{id}/content. New captures return a GUID-based fileName and download filename instead of the original filename stem, FRA/AMS label and capture identity. Related files retain suffixes such as .thumbnail.jpg and .json. Existing stored captures remain readable under their existing names. Original device names and origin regions remain in the metadata JSON.
Migration: treat returned filenames as opaque; do not parse region, device filename or capture identity from them. Continue using API capture identifiers and content links. The filename GUID is not the capture ID. Request fields, response field types and authorization are unchanged. Prepared for beta deployment; production pending.
Negative icon counts display an absolute-value notification badge
Endpoints:GetIconFromSet — GET /api/v1/icon/{iconSet}/{kid}.{format}, GetIconImageFromSet — GET /api/v1/icon/{iconSet}/{kid}/{size}.{format} and GetIconImageWithBackgroundFromSet — GET /api/v1/icon/{iconSet}/{kid}/{backColor}/{size}.{format}. Affected: rendered notification badge for the signed count encoded in the icon KID.
Previously, zero and negative counts hid the badge, and positive counts used a red background with white digits. Zero still hides it; negative counts now show the full absolute value with a red background and white digits. Positive counts now use a white background and black digits. All signed Int64 values, including Int64.MinValue, remain accepted. KID encoding, response media types and operation IDs are unchanged.
Customer action
If negative counts previously suppressed a badge in your integration, encode count zero instead. To retain the former red badge for a positive count, encode its negative value; otherwise retain the positive count for the white badge. Do not compute the absolute value using signed Int64 arithmetic for its minimum value. Portal clients 0.7.2 include the current contract documentation. Verified on beta on 2026-10-14 at commit 747c9e9; production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Packed unit types retain all eight type bits
GetLocationUnits — GET /api/v1/locations/{locationKid}/units, GetUnitOverview — GET /api/v1/units/{unitKid}, GetUnitGroup — GET /api/v1/units/{unitKid}/groups/{kind}/{group}, GetUnits — GET /api/v1/units, GetTerminals — GET /api/v1/terminals and controller overview/group equivalents now decode legacy UnitType as (value >> 1) & 255, previously & 63. For example, raw 132 now identifies type 66 instead of type 2. Response field types are unchanged, but unitType, unitTypeName, available setting/state groups and directory type filtering/sorting can change. A present direct UnitType2 still takes precedence, including malformed values returning null without fallback.
Migration: use the returned type and group lists, discard cached type metadata and restart directory pagination. Refresh setting revisions before writing: edits and history are governed by the corrected type, and previously accessible groups may return 404. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Unit progress uses only elapsed expected time, clamped to 0–100%
GetLocationUnits — GET /api/v1/locations/{locationKid}/units, GetUnitOverview — GET /api/v1/units/{unitKid}, GetUnitGroup — GET /api/v1/units/{unitKid}/groups/{kind}/{group}: progress.percent now uses floor(clamp((now − Started)/(Done − Started) × 100, 0, 100)). Previously lifecycle, connection and sequence state could suppress the percentage, estimated progress stopped at 99, and 100 required DONE. Valid timing now returns status Estimated and 0–100 regardless of those states; at/after the expected end percent is 100 and remainingSeconds is 0. Before start, percent is 0. Missing/invalid times return UnknownEndTime and null percent/remainingSeconds. Field types are unchanged. The same calculation is available through the new optional fields=progress on GetUnits — GET /api/v1/units.
Migration: accept Estimated with 100 percent; do not infer physical completion from progress status or percent. Use cycle for device state. Remove assumptions that elapsed estimates return EstimateExpired or that disabled/disconnected units always have null percentages. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Controller identities use MainId and retain the first unit as a child
GetLocationUnits — GET /api/v1/locations/{locationKid}/units, GetUnitOverview — GET /api/v1/units/{unitKid}, GetUnitGroup — GET /api/v1/units/{unitKid}/groups/{kind}/{group}, GetUnits — GET /api/v1/units and GetTerminals — GET /api/v1/terminals: optional terminal.kid previously identified the main Unit (UnitId). It now identifies a Controller (MainId, UnitId=0). The terminal.name field now uses the main unit’s eState.ComputerName, matching GetTerminals, instead of eSetting.Name; absent ComputerName returns an empty string. Nullability and icon fields are unchanged; each row's own unit KID is unchanged. GetUnits query terminalKid now requires this Controller KID instead of a Unit KID, and includes the first unit as well as mapped children instead of excluding it. Old Unit KIDs return 400 invalid-terminal.
Migration: refresh cached names and hierarchy metadata, display terminal.name as reported computer text (handle empty names), group by the returned terminal.kid, and pass it unchanged as terminalKid. Keep the first unit as its own child; do not compare its KID to the controller KID. Restart directory cursors and discard stale cached parent identities. Controller overview/group routes use the Controller KID; their unit.kid remains the underlying storage unit. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Self-registration rejects unsupported email languages
RequestManagerSignUp — POST /api/v1/session/signup: optional nullable language now accepts only the exact lowercase strings en, da and es when a non-null value is supplied. Previously any string was accepted, with values other than exact da/es silently using English. Empty strings, whitespace, regional tags, uppercase and unknown values now return HTTP 400 ValidationProblemDetails with errors.Language before account creation or mail queueing. Omitting the field or sending null still uses English; valid requests retain the existing response contract.
Migration: map browser or application language preferences to one of the three supported lowercase codes. For English, send en, omit the field or send null; do not send an empty string or an unprocessed regional language code. Handle language validation errors before resubmitting. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Public purchase amounts are grouped by currency
GetPublicPurchases — GET /api/v1/public/statistics/purchases: the top-level decimal amount and nullable string currency are removed. Previously all qualifying purchase amounts were combined and labelled with MAX(Currency). The response now contains a non-null currencies array of {currency, count, amount} objects: one group per currency, with a nullable string currency, integer purchase count and decimal amount in major currency units, calculated as -SUM(Amount)/100 within that group. Missing currency remains null in its own group. No combined monetary total or conversion is returned.
The top-level count remains the total number of purchases and equals the sum of group counts. With no purchases, the former amount: 0, currency: null are replaced by currencies: []; count remains zero. tenantKid, lookbackHours, sinceUtc, measuredAtUtc, the request, filters and access rules are unchanged.
Migration: iterate over currencies and display each group's amount with its own currency; use the top-level count for the overall purchase count. Handle an empty array and a null currency without assuming a known currency. Stop reading the removed top-level fields and do not sum amounts across currencies. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Beta 2026-10-13 · 353b2ae · clients 0.7.1 · production pending
Self-registration creates a pending manager before email delivery
RequestManagerSignUp — POST /api/v1/session/signup: required request fields password and confirmPassword added. Previously email/name/organisation queued proof without creating an account; now the password is chosen immediately and a disabled manager plus manager-bound verification mail are committed atomically. Existing-address processing remains non-disclosing 202 accepted.
ConfirmManagerSignUp — POST /api/v1/session/signup-confirmation: password and confirmPassword removed; request contains only token. Previously confirmation created a manager and returned 201 manager-created; now it activates the existing pending manager and returns 200 email-verified. No business access is granted.
Migration: collect matching 12–128 printable ASCII passwords on the initial form, send them with signup, send only the verification token on confirmation, and expect 200/email-verified. Tokens from the previous local flow are invalid; request a new link. Verified on all five beta apps on 2026-10-13 at commit 353b2ae; packaged Portal clients 0.7.1 are synchronized. Production pending.
Release 2026-10-10 · available on this host · clients 0.6.1
Terminal directory names use the reported computer name
GetTerminals — GET /api/v1/terminals: items[].name now reads eState.ComputerName from tenant-bound Log24 instead of eSetting.Name. Missing computer names return an empty string; there is no settings-name fallback. Name filtering and sorting use the new source. Optional string fields/sorts added: versionMinor, bootReason, booted, firmware, storageCardSerialNumber, page, backLight. They are null unless requested through fields; selected missing values are empty strings. Booted uses eSetting.Booted, the other added values use eState.
Migration: treat name as the reported computer name, handle empty names, and restart pagination after release. Existing stored names are preserved. GetUnits and UserBalance name behavior is unchanged.
Release 2026-10-10 · available on this host · clients 0.6.1
Bank and location directory tab assignments corrected
GetLocations — GET /api/v1/locations and GetActiveLocationCount — GET /api/v1/locations/active-count now require Locations1 (65), previously Banks2 (5). Without tab 65 these operations return 403. The new unreleased GetBanks — GET /api/v1/banks and GetActiveBankCount — GET /api/v1/banks/active-count require Banks2 (5), previously Locations1 (65).
Migration: review and explicitly assign tab 65 for location directory access and tab 5 for bank directory access. Stored assignments and numeric enum IDs are not rewritten. Resource scopes and operation permissions still apply. Request and response fields are unchanged; active-count iconKid values use the corresponding tab metadata. UserBalance is unchanged.
Release 2026-10-10 · available on this host · clients 0.6.1
Tab 66 is named Terminals1
GetCurrentManager — GET /api/v1/session/me, GetManager — GET /api/v1/managers/{managerKid}, SetManagerTab — POST /api/v1/managers/{managerKid}/tabs/{tabId} and SetManagerPermissionRole — POST /api/v1/managers/{managerKid}/permission-role: tab objects with id:66 now use name:"Terminals1" instead of name:"Disp66", including the available-tab catalog. The numeric identity and stored tab assignments remain unchanged.
Migration: identify tabs by their stable numeric ID, update any name comparisons for tab 66, and treat its terminal-directory access as tenant-bound. This does not automatically assign tab 66 or grant resource/operation permissions. Clients 0.6.1 include this contract. UserBalance is unchanged.
Release 2026-10-10 · available on this host · clients 0.6.1
Role replacement requires Tabs and Kids permissions
SetManagerPermissionRole — POST /api/v1/managers/{managerKid}/permission-role: every role now requires the caller's Read and Write permissions on both Tabs and Kids, checked from the fresh authorization snapshot inside the transaction before any write. Previously only accounting checked Tabs and no role checked Kids, although all roles replaced both permission masks. Newly rejected requests return 403 with missing-tabs-read, missing-tabs-write, missing-kids-read or missing-kids-write. Request and success-response fields are unchanged.
Migration: require both categories' Read/Write permissions before offering role replacement, handle these 403 codes and obtain the missing permissions through an authorized administrator. Existing manager, tenant and own-edit checks remain. UserBalance is unchanged.
Release 2026-10-07 · available on this host · clients 0.5.2
Default manager retention is 10 days
GetCurrentManager — GET /api/v1/session/me, GetMyManagerProfile — GET /api/v1/session/me/profile, GetManagers — GET /api/v1/managers and GetManager — GET /api/v1/managers/{managerKid}: retentionDays now defaults to 10 when the stored setting is absent or SQL NULL, previously 0. Explicit values, including 0, are preserved; malformed or negative values still resolve to 0. This also changes the default visibility window for otherwise authorized deleted records; no records are physically deleted or migrated. Integrations should use the returned value; save an explicit 0 if deleted records must stay hidden.
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-07 · available on this host · clients 0.5.2
Remove service permission, KID and tab assignments
GetService — GET /api/v1/services/{serviceKid}, SetServiceProfileField — POST /api/v1/services/{serviceKid}/profile/{field} and GenerateServiceApiKey — POST /api/v1/services/{serviceKid}/api-key: remove the previously unreleased access object (nested under details for key generation), including resourceGrants, tabs, availableTabs, operationPermissions, kidsRevision, tabsRevision, canEdit, canEditTabs and canEditKids.
Removed operations: SetServicePermission — POST /api/v1/services/{serviceKid}/permissions/{resource}, SetServicePermissionRole — POST /api/v1/services/{serviceKid}/permission-role, SetServiceTab — POST /api/v1/services/{serviceKid}/tabs/{tabId} and SetServiceKid — POST /api/v1/services/{serviceKid}/kids. Their request bodies and response contracts are no longer supported.
Migration: remove service access editors and stop sending assignment requests or reading access fields. Each business endpoint explicitly authorizes the authenticated eUserId; synchronization requires ServiceSync. Service authentication, account state, key revocation, tenant isolation and manager authorization for service administration remain enforced. Existing stored access settings are preserved but ignored for service authorization. The Tabs/Kids entry below describes the final manager-only contract. UserBalance is unchanged. Clients 0.5.2 include this contract.
Release 2026-10-07 · available on this host · clients 0.5.2
Logo parts use URL path segments
GetKombineLogo, GetKombineLogoSized and GetKombineLogoWithBackground — GET /api/v1/logos/kombine/{color}.svg, /{color}/{width}.svg and /{color}/{background}/{width}.svg: the earlier local, unreleased optional parts query parameter has been removed. These routes return the complete symbol.
Migration: replace /api/v1/logos/kombine/black.svg?parts=W with /api/v1/logos/kombine/W/black.svg (GetKombineLogoParts). The intermediate unreleased /kombine/parts/{parts}/... routes have also been replaced by /kombine/{parts}/...; remove the literal parts/ segment. Combinations use the same segment, for example /api/v1/logos/kombine/NS/black.svg. Width and background/width variants use GetKombineLogoPartsSized and GetKombineLogoPartsWithBackground. All parameters precede the final .svg. Parts accepts 1–4 letters N/E/S/W, case-insensitively; invalid selections return 400 with invalid-logo-parameters. SVG geometry and existing full-symbol contracts are unchanged. Clients 0.5.2 include this contract.
Release 2026-10-07 · available on this host · clients 0.5.2
Independent Tabs and Kids permissions; nine-category role requests
SetManagerPermissionRole — POST /api/v1/managers/{managerKid}/permission-role: expectedFlags previously required seven categories; now exactly nine are required, adding Tabs and Kids. Incomplete requests return 400 invalid-permission-role. Role responses and operationPermissions projections add these two resources; match by name rather than position. Full support presets set both to 63; accounting/caretaker set both to Read (1). Existing stored values are preserved; absent values default to Read, malformed values grant nothing.
SetManagerTab — POST /api/v1/managers/{managerKid}/tabs/{tabId} and SetManagerKid — POST /api/v1/managers/{managerKid}/kids now additionally require the caller's respective Tabs/Kids Read and Write. Applying accounting also requires Tabs Read/Write. Previously these operations relied on the parent Managers edit permission and existing scope checks. New 403 codes: missing-tabs-read, missing-tabs-write, missing-kids-read, missing-kids-write. Existing parent, tenant and own-edit checks remain.
Migration: reread the object and send all nine expectedFlags, with explicit null for invalid masks. Handle independent manager canEditTabs/canEditKids hints. Role canEditTabs may differ from canEditPermissions. Grant the new write flags explicitly through an authorized permission editor before changing assignments; there is no automatic data migration. UserBalance is unchanged. Clients 0.5.2 include this manager contract.
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 ID
Unchanged method/path
GetCurrentManager
GET /api/v1/session/me
GetMyManagerProfile
GET /api/v1/session/me/profile
SetMyManagerProfileField
POST /api/v1/session/me/profile/{field}
GetManagers
GET /api/v1/managers
GetManager
GET /api/v1/managers/{managerKid}
SetManagerProfileField
POST /api/v1/managers/{managerKid}/profile/{field}
SetManagerTab
POST /api/v1/managers/{managerKid}/tabs/{tabId}
SetManagerPermissionRole
POST /api/v1/managers/{managerKid}/permission-role
GetInstallers
GET /api/v1/installers
GetInstaller
GET /api/v1/installers/{installerKid}
SetInstallerIcon
POST /api/v1/installers/{installerKid}/icon
GetServices
GET /api/v1/services
GetService
GET /api/v1/services/{serviceKid}
SetServiceProfileField
POST /api/v1/services/{serviceKid}/profile/{field}
GenerateServiceApiKey
POST /api/v1/services/{serviceKid}/api-key
GetLocations
GET /api/v1/locations
GetBankLocations
GET /api/v1/banks/{bankKid}/locations
GetLocationUnits
GET /api/v1/locations/{locationKid}/units
GetUnitOverview
GET /api/v1/units/{unitKid}
GetUnitGroup
GET /api/v1/units/{unitKid}/groups/{kind}/{group}
SetUnitSetting
POST /api/v1/units/{unitKid}/groups/settings/{group}/{setting}
GetUnitSettingHistory
GET /api/v1/units/{unitKid}/groups/settings/{group}/{setting}/history
GetBankUsers
GET /api/v1/banks/{bankKid}/users
GetBankUserWorkspace
GET /api/v1/banks/{bankKid}/users/{userKid}/workspace
CreateBankUser
POST /api/v1/banks/{bankKid}/users
ExecuteBankUserCommand
POST /api/v1/banks/{bankKid}/users/{userKid}/commands
GetBankBookings
GET /api/v1/banks/{bankKid}/bookings
ExecuteBankBookingCommand
POST /api/v1/banks/{bankKid}/bookings/{bookingKid}/commands
GetBankAccount
GET /api/v1/banks/{bankKid}/account
GetBankDocuments
GET /api/v1/banks/{bankKid}/documents
GetUnitDocumentTable
GET /api/v1/documents/{documentKid}/table
GetTenantStatus
GET /api/v1/tenant/status
SearchBanks
GET /api/v1/search/banks
SearchBankActivation
GET /api/v1/search/bank-activation
GetSearchBank
GET /api/v1/search/banks/{bankKid}
SearchLocations
GET /api/v1/search/locations
SearchLocationActivation
GET /api/v1/search/location-activation
SearchUsers
GET /api/v1/search/users
SearchUserSms
GET /api/v1/search/user-sms
SearchUserActivation
GET /api/v1/search/user-activation
Migration: rename response model properties and pass the supplied string unchanged, URL-encoded, to /api/v1/icon/{iconSet}/{kid}.svg. Stop interpreting every value as an enum name or composing KIDs in the portal. Use public GetIconPresentation — GET /api/v1/icon/presentation for presentation options and a fresh calendar identity. Icon setting writes and availableIcons still use enum names; service objects also expose iconName for selection. GetActiveLocationCount (GET /api/v1/locations/active-count) additionally returns a ready-to-render iconKid with its badge. Permissions and tenant binding are unchanged; these values grant no access.
Release: 2026-10-01, client version 0.2.1. Generated clients, OpenAPI snapshots and downloadable packages are synchronized with this contract. This release label applies to the version served by this host; beta and production are promoted independently. Existing image paths and image caching are unchanged.
Release 2026-10-01 · available on this host
Icon images · Canonical KID with icons, count, color and text; required asset set
Affected requests:kid previously accepted an eIcon name, numeric value/index or substring. It now first parses a canonical Kombine.Flex.Kid.ToString() and reads Kid.Icons, Kid.Count (Int64), Kid.Color and Kid.Text. Separate count/color/text/sub path fields are removed. Color uses the low 24 bits as opaque RGB (0 = black; the high byte is ignored, as for Flex eColor). Text is UTF-8, case-sensitive and limited to 128 characters without controls. Encoded canonical KIDs are limited to 2048 characters. If that fails, an exact case-insensitive eIcon name is accepted with count zero, black and empty text. Numeric/index/substring enum lookup is no longer a fallback; invalid input returns 400. The first list entry is the main icon and the second is the under-icon. A missing/none second entry means no under-icon; later entries do not affect rendering. Every list entry must be a defined eIcon value. An empty list uses eIcon.none. Other KID fields cause no business lookup or permission change. Count ≤ 0 hides the badge. Positive counts display in full in a red capsule whose straight middle widens between circular ends. Very long labels widen the SVG canvas; no count is abbreviated.
Operation ID
Previous method/path
New method/path
GetIconFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GET /api/v1/icon/{iconSet}/{kid}.{format}
GetIconImageFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{backColor}/{size}.{format}
GetIcon2Svg (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
Use GetIconFromSet with explicit line or g.
GetIcon2Image (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
Use GetIconImageFromSet with explicit line or g.
GetIcon2ImageWithBackground (removed)
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Use GetIconImageWithBackgroundFromSet with explicit line or g.
Customer action
The interim local route /api/v1/icon/{iconSet}/{kid}/{sub}.{format} is also replaced by /api/v1/icon/{iconSet}/{kid}.{format}. Append the former sub value as the second entry of Kid.Icons and remove its path segment; the size/background forms remove that segment in the same way.
Add the main icon and optional under-icon to Kid.Icons in that order, set Count, Color and Text on a Kid, URL-encode ToString(), remove the count/color/text/sub segments and select the set explicitly (line preserves the former default). For example, Kid.Icons = [house, check], Count = 7, Color = 0x336699, Text = "A12" produces 413132x7qE20i11Bi336699Ic: use /api/v1/icon/line/413132x7qE20i11Bi336699Ic.svg. For count zero, black and empty text, /api/v1/icon/line/house.svg also works. Update stored links/builders; old shapes are not aliases and overlapping paths can be interpreted as different requests. Rendering formats, set fallback, cache/304 behavior and the public asset catalog remain unchanged. Unknown sets/missing assets return 404; invalid parameters return 400.
Release: 2026-10-01, client version 0.2.1. Generated clients, OpenAPI snapshots and downloadable packages are synchronized with this contract. Upgrade the client and migrate the removed routes as described above. This release label applies to the version served by this host; beta and production are promoted independently.
Release 2026-09-28 · available on this host
Account2 · Retention-aware identities, decoded descriptions and reversal eligibility
Operation ID
Method/path
GetBankAccount
GET /api/v1/banks/{bankKid}/account
ExportBankAccount
GET /api/v1/banks/{bankKid}/account/export
GetBankAccountRevision
GET /api/v1/banks/{bankKid}/account/revision
ReverseBankAccountEntry
POST /api/v1/banks/{bankKid}/account/{transactionKid}/reversal
Previous: listing/export did not apply tenant/manager retention settings. userKid, userName and userNumber could identify expired entries; descriptions could contain partial legacy text or internal payment markers. Ordinary reversal eligibility did not explicitly reject expired or payment-managed consumption.
New: tenant LawAccountingYears and LawSurveillanceDays, with positive manager overrides, mask expired identities in general views: userKid becomes the bank's GDPR user KID (UserId 1000), userName/userNumber become empty strings, description contains only the transaction type, isAnonymized is true and canReverse is false. Zero/missing/invalid retention values retain no identity. With an explicit userKid, expired entries are excluded from rows and totals. CSV/XLSX applies the same rules with unchanged columns. Descriptions use the full shared decoder; internal payment IDs are removed. Payment-managed and expired entries return 422 reversal-unavailable on ordinary reversal. Revision values now include retention visibility and caches are separated by manager. Amount representation and request fields remain unchanged.
Migration: display an anonymous label when isAnonymized is true and never link it to a resident profile. Do not interpret description text as a payment identifier; use the additive paymentKind field (Credit, ReserveRefund, Managed or empty). Honor canReverse and handle 422 without retrying. Do not merge pages with different revisions. The additive documentKey/documentId and documents fields group only filtered lines on each page; merge groups by key across pages, not DocId alone. Existing flat items and row pagination remain supported.
Release: 2026-09-28. The clients and downloadable packages have been synchronized with this contract; package version 0.1.0 is retained and no external registry publication is claimed.
Release 2026-09-28 · available on this host
Icon images · Missing assets are searched in other local sets
Affected responses: an icon missing from the selected set previously returned HTTP 404 even when another local set contained it. Rendering now searches for the same eIcon identity in the selected set first, then other packaged sets in ordinal alphabetical order. Each main/under-icon resolves independently. A matching asset returns HTTP 200 in the requested format, or 304 for a matching conditional request. Unknown sets and icons absent from every local set still return 404. Request fields, format encodings and existing images in the selected set are unchanged. There is no external-server or database fallback. Included in release 2026-09-28.
Operation ID
Method/path
GetIcon2Svg
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2Image
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackground
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GetIconFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Do not interpret a successful render as proof that the asset belongs to the requested set. Use the documentation set catalogs to inspect actual membership. For example, /api/v1/icon/g/house/black/0/0/none.svg now renders the house asset from line. Paths without a set still prefer line. Clients that implement their own 404-based set search can rely on server-side lookup instead. Refresh rendered images or allow the existing ten-minute browser cache to expire; renderer build changes invalidate disk-cache entries.
Endpoints:GET /api/v1/logos/kombine/{color}.svg, GET /api/v1/logos/kombine-text/{color}.svg and GET /api/v1/logos/kombine-logo-text/{color}.svg. The SVG response root previously had width="256" and a proportional numeric height (256, 48.162712 or 51.2). These attributes are now omitted. The unchanged viewBox and preserveAspectRatio="xMidYMid meet" let the complete artwork fit and center in its viewport without cropping or stretching. Routes with an explicit width retain their pixel dimensions. Colors, geometry, content type, operation IDs and status codes are unchanged. Included in release 2026-09-28.
Customer action
If your layout or SVG parser requires fixed dimensions, use the corresponding /{color}/256.svg route, or specify dimensions on the embedding element. Do not assume the unsized response contains numeric width/height attributes. Renderer cache keys have changed; clients can refresh or wait for the existing 600-second browser cache to expire.
Release 2026-09-28 · available on this host
LoginManager · Duplicate credentials now select one active manager
Endpoint:POST /api/v1/session/login. Affected: the status and account selection when multiple managers match both email and password. Previously this case returned HTTP 401. It now returns HTTP 200 for the active, non-deleted manager with the newest eSetting.Alive krumb MS2000, after atomically clearing Password on the other matching managers. Activity uses the krumb timestamp, not Text; missing or invalid timestamps rank last. Equal timestamps, including all unknown, are resolved by the lowest UserId. The selection uses a fresh locked read in the cleanup transaction. Different passwords sharing an email are unchanged. No active match means no cleanup and the lowest matching account determines the existing HTTP 403 account-state error. Cleanup/storage failures or more than 100 matching rows return HTTP 503. Request fields and the token response representation are unchanged. Included in release 2026-09-28.
Customer action
Do not rely on duplicate credentials returning 401. Call GetCurrentManager after login and use its identity and permissions; grants from duplicate accounts are not combined. Sessions of accounts whose passwords are cleared become invalid, subject to the existing maximum 60-second cache on other API instances. To keep distinct accounts usable, give them distinct credentials before this change is released. Do not automatically retry an uncertain login/cleanup response.
Affected requests: remove the literal sets segment after /api/v1/icon/. The previous set-specific paths are removed. The operation IDs, parameter names, rendering, response formats and cache behavior are unchanged. Included in release 2026-09-28.
Operation ID
Previous method/path
New method/path
GetIconFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIconImageFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIconImageWithBackgroundFromSet
GET /api/v1/icon/sets/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
GET /api/v1/icon/{iconSet}/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Remove sets/ from set-specific image URL builders and stored links. For example, use /api/v1/icon/line/house/000000/7/A12/none/128.svg. Keep the set name and all other path values. Existing icon-first routes without a set name still use line; known set names take precedence where route shapes overlap. The rebuilt clients in release 2026-09-28 use these paths.
Release 2026-09-28 · available on this host
GetIcon2Svg / GetIcon2Image / GetIcon2ImageWithBackground · Icon paths and format parameter
Affected requests: the URL prefix for all three GET image operations changes from /Icon2 to /api/v1/icon. The former routes are removed. Operation IDs are retained. The extension parameter is now named format instead of fileType on the sized routes; it is a required path parameter. The short route replaces its fixed .svg suffix with required .{format} and also accepts raster formats, using 128 × 128 pixels when no size is supplied. Existing SVG and sized-image rendering semantics, content types and conditional caching are unchanged. Included in release 2026-09-28.
Operation ID
Previous method/path
New method/path
GetIcon2Svg
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}.svg
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}.{format}
GetIcon2Image
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}/{size}.{fileType}
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{size}.{format}
GetIcon2ImageWithBackground
GET /Icon2/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{fileType}
GET /api/v1/icon/{kid}/{color}/{count}/{text}/{sub}/{backColor}/{size}.{format}
Customer action
Update image URL builders and stored links to use /api/v1/icon/. Use format when binding parameters by their OpenAPI names, and pass svg explicitly for the former SVG-only operation. Keep presentation values in their existing path segments; no query parameters are required. For example, use /api/v1/icon/house/000000/7/A12/none.svg or /api/v1/icon/house/000000/7/A12/none/128.png. Continue URL-encoding individual path values. The rebuilt clients in release 2026-09-28 use these paths.
Release 2026-09-28 · available on this host
GenerateServiceApiKey · Generated keys now start with kt_
Affected: the response field apiKey. Previously, generated keys started with k followed by 64 random ASCII letters and digits (65 characters total). New keys start with kt_ followed by the same 64-character random payload (67 characters total). The payload still includes both uppercase and lowercase letters. Keys remain case-sensitive. The request and all other response fields are unchanged. Included in release 2026-09-28.
Customer action
Allow the underscore and the new total length in fields and validators that consume generated keys. Preserve the exact value returned by the API, including the prefix. Prefer treating keys as opaque strings. Do not add or replace a prefix on existing keys: their stored hashes and login behavior are unchanged. Both existing keys and new kt_ keys remain usable with Equipment service login until replaced or revoked. Client packages were rebuilt for release 2026-09-28.
Release 2026-09-28 · available on this host
SetServiceProfileField · ApiKeyHash no longer accepts manual writes
Affected: the field path parameter and the response to manual hash writes. Previously, ApiKeyHash accepted a caller-supplied hash, or an empty value to clear it, and returned HTTP 200 on success. Only Name and Icon are now accepted. Requests with field=ApiKeyHash return HTTP 400 with problem code invalid-service-profile, regardless of the supplied value. No hash is written or cleared. Included in release 2026-09-28.
Customer action
Remove manual hash editing and clearing. To replace a service key, read its latest profileRevision with GetService, then call GenerateServiceApiKey at POST /api/v1/services/{serviceKid}/api-key:
{"expectedRevision":"<profileRevision from GetService>"}
Use the returned apiKey only after a successful HTTP 200 response and keep details.profileRevision for later edits. Generation replaces the existing key; the plaintext key is returned once. Do not retry automatically after an uncertain response. Manual key import and clearing are no longer supported. Existing stored hashes and response shapes are unchanged. Client packages were rebuilt for release 2026-09-28.
Locations1 location directory
One bank: Both GetLocations and GetActiveLocationCount accept optional bankKid, a canonical bank KID in the site tenant. It intersects current grants before search, sorting and pagination; it never grants access or changes site-wide privileges. Example: GET /api/v1/locations?bankKid={bankKid}&fields=address&enabledOnly=true. Repeat the same bankKid with every cursor; changing it returns 400 invalid-cursor. Invalid or wrong-tenant bank KIDs return 400 invalid-bank. Omit it for the existing cross-bank view. Implemented locally, unreleased.
Filtered location count (unreleased)
GetLocationCount — GET /api/v1/locations/count returns {"count":123} for the complete filtered result. Use the same filter, fields, enabledOnly, bankKid, sort and direction as GetLocations. Selected fields affect search, even when they are empty in some rows. The operation reuses Locations1, Bank Read, Location Read, tenant/resource scope, retention, KID/code lookup and code-access validation. It counts all matching states allowed by those rules; it is not the active navigation badge. No cursor or pageSize applies.
Start counting in parallel with data retrieval. Keep progress unknown until count is available; never treat failure as zero. HTTP 400 means invalid options, 401 requires login, 403 means missing scope/permissions, and 503 locations-unavailable means storage failure or the 12-second deadline. No-store. Count is exact at query time, not a snapshot spanning later pages. Concurrent edits or permission changes may change the final total; do not show a false 100%. Cancel a pending count when retrieval finishes. This endpoint is locally implemented and unreleased; generated clients/packages remain deferred to beta preparation.
Filtered export counts for every directory
Excel export starts at 100 rows and adapts each subsequent page between 10 and 250 rows using measured API response time, targeting 2.5 seconds. Each data request has a 3-second cancellation timeout. A timed-out read retries the same cursor with half as many rows; if the minimum size also times out, the export fails without publishing a partial workbook. Private directory pageSize may change with a cursor; all filters, sorting, selected fields and scope must stay unchanged. The separate count request retains its own deadline. Normal list defaults and balance batch limits are unchanged. Existing unit/terminal cursors created before this update require restarting without a cursor (400 invalid-cursor). Unreleased.
These endpoints return {"count":123} for the complete authorized export selection. Send the same filter, sort, direction as the listing; banks and units/terminals also accept enabledOnly, fields; banks accept bankType; units/terminals accept locationKid, terminalKid, runningOnly (units also autoOutOfOrderOnly); installers accept includeActivationCode; resident search accepts enabledOnly; bank residents accept locationKid, deleted. Do not send a cursor .
Every count rechecks the listing authorization, scope, retention and filters. Banks, locations and units/terminals use direct SQL aggregates. Resident search counts whole-bank access in SQL; location-only grants stream only identity/access data through the shared legacy/JSON Locations decoder, without details or page enumeration. Administrators, installers and bank residents count one minimal filtered index (existing indexes may be up to 60 seconds old); services use their finite catalog. Existing storage deadlines and bounds still return 503 instead of partial counts. Start count concurrently with retrieval, show fetched rows while pending/failed, then switch to percentage. HTTP 400 means invalid options, 401 requires login and 403 means missing access. No-store. No snapshot spans subsequent pages, so concurrent edits can invalidate a count. Navigation badges retain their distinct semantics. Locally implemented, unreleased; clients/packages follow at beta preparation.
Active location count
GetActiveLocationCount — GET /api/v1/locations/active-count. Returns {"count":123} for the Lokationer/Locations1 icon. Uses the same active-manager, Locations1, 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 white capsule widens to fit the digits.
GetLocations — GET /api/v1/locations lists accessible locations across banks. Requires active manager, Locations1 (65), Bank Read, Location Read and matching site/bank/location grants, rechecked on every page. Location-only grants never expose siblings. With an explicit site-wide grant, all location states are listed, including disabled locations and old deletions; parent-bank deletion does not hide them. With limited grants, only locations whose Enabled value is exactly 1 are listed, and both bank and location deletion follow RetentionDays. Missing Enabled is not enabled. Deleted=0 or missing is visible; positive timestamps must fall within the inclusive window from now minus RetentionDays through now. Zero retention hides all deleted entries; malformed or future deletion values are hidden for limited grants. Reads only site Log24; banks below 1000 are excluded. Discovery requires a Name, Icon, VismaCustNo, Enabled or Deleted location setting.
enabledOnly=true excludes locations whose Enabled value is not exactly 1, before paging. The default is false. This only narrows the authorized results: existing deletion/RetentionDays rules and activation-code permissions still apply. Enabled deleted locations remain visible when authorized. Keep this value on every continuation request; changing it requires restarting without a cursor (otherwise 400 invalid-cursor). Example: GET /api/v1/locations?enabledOnly=true&sort=name&direction=asc&pageSize=50.
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.
Map coordinates:includeCoordinates=true additionally returns latitude and longitude without adding these columns to text search or changing the response fields selection. Default: false. Example: GET /api/v1/locations?fields=address&filter=street&includeCoordinates=true. The same location permissions and nullable decimal-degree rules apply. Keep this option unchanged with each cursor; changing it returns 400 invalid-cursor, so restart without the cursor. Follow all pages; locations without valid coordinates cannot be plotted.
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, soapEnd. 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. soapEnd reads the saved date text from location-owned eSetting.SoapEnd (3025), like vismaStart; it is not MS2000. It sorts as stored text and is displayed unchanged, with missing values blank and unselected values null. Select it with GET /api/v1/locations?fields=soapEnd&sort=soapEnd; existing manager, scope, retention and cursor rules apply. Implemented locally, unreleased. 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.2.
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.
Create an administrator
Unreleased:CreateManager, POST /api/v1/managers, has no request body. Requires Managers1, independent Managers Read and Create, and a whole-tenant grant. Write alone does not grant Create. The actor's account, credential stamp and permissions are read afresh before allocation and again inside the write transaction, with locks held through commit. The site determines the tenant.
The Location response header points to GetManager. The new administrator is enabled and not deleted, with no assigned password, email, tabs or scopes. No invitation is sent. Use GetManager and the existing profile, permission, scope and tab operations to configure it; these edits have their own authorization requirements, including Managers Write.
Allocation finds the next never-used manager ID above the greatest ID in history/current Log7 and atomically appends Enabled=1 and Icon=businessperson. The new administrator starts with the businessperson icon; existing administrators keep their stored icons. No Deleted setting is written; deleted IDs and abandoned reservation markers are never reused. A tenant-specific MySQL advisory lock is acquired before allocation and held through commit on the same writer connection. Waiting for the lock is limited to five seconds within the 15-second operation deadline; failure to acquire it returns 503 without creating a manager. No table lock, schema change or random marker is used. All creators, including AMS and FRA, must use the same lock on the same MySQL writer. Independent writers are not supported by this lock; modulo allocation by ServerId, using a fixed common modulus and a unique residue per writer, is the agreed future approach if needed and is not implemented yet.
401: log in again. 403: missing-managers-tab, missing-managers-read, missing-managers-create or missing-tenant-access; obtain the required access. 409: manager-ids-exhausted (identity range exhausted). 503: manager-creation-unavailable. Application errors use ProblemDetails with code; 401 can have no body. Creation is not idempotent: after a timeout, cancellation or lost response, inspect the directory before a manual retry. Never automatically resubmit creation.
Gravatar
Administrator avatars: GetManagers, GetManager and SetManagerProfileField include nullable gravatarUrl. Gravatar is preferred; the stored icon, or the standard person icon when none is selected, is the fallback. When Email is valid, the API returns https://www.gravatar.com/avatar/{sha256}?s=96&d=404&r=g, using the trimmed, lowercase email. Render it as an image with referrerpolicy="no-referrer"; on 404 or network failure use iconKid instead. Null means use iconKid directly. Refresh the avatar after Email or Icon changes. This is a presentation hint, not proof of identity. Direct browser requests disclose the email hash and client IP to Gravatar. No Gravatar account, API key, profile lookup or database write is required. The portal uses this fallback in administrator lists, detail headings and workspace shortcuts. Unreleased; packaged clients are updated during beta preparation.
GetManagersWithSameEmail
GetManagersWithSameEmail: GET /api/v1/managers/{managerKid}/same-email returns [{"kid":"canonical manager KID","name":"display name"}]. Requires the same active session, Managers1, Managers Read and whole-tenant access as GetManager. The source must be visible (otherwise 404). Matches use exact trimmed email, ignoring case; empty emails and the source are excluded. Disabled managers remain eligible, deleted managers follow caller retention. The candidate index can be 60 seconds old; matching candidates are reread from current Log7. Maximum 100 candidates, 20,000 indexed entries and a 12-second deadline; larger results fail with 503 manager-directory-too-large, never a truncated list. 400 invalid-manager-kid, 401 login required, 403 missing access, 503 managers-unavailable for storage failure. Responses use no-store. Example: curl -H "Authorization: Bearer $TOKEN" "$API_BASE/api/v1/managers/$MANAGER_KID/same-email". The portal fetches the list only when the side panel is visible, shows links only when matches exist, and invalidates the list after saving Email or navigating to a manager. Refresh waits until the panel is visible. Unreleased; generated clients follow at beta. Each match also includes iconKid and nullable gravatarUrl: show Gravatar first, then the icon, or the standard person icon when unset.
Bank directory and account currency icon
List authorized banks and render the most frequent authorized ledger currency in the account icon.
GetBanks: GET /api/v1/banks and GetActiveBankCount: GET /api/v1/banks/active-count require Banks2 (5), Bank Read and site grants. Core values: kid, name, iconKid, enabled, deleted, deletedAt. Optional fields=bankType,bankActivationCode,exportFormat,settlementEmails,settlementDays,bankDays,cluster,hardware are returned in each item's fields dictionary; the response fields array lists the selected keys. BankDays and SettlementDays are separate stored settings. Bank-wide metadata requires whole-bank or whole-tenant access; location-only grants expose parent identity and null optional values. Bank activation code display/search/sort additionally requires whole-tenant access and Bank Create. Location activation codes remain separate in GetLocations.The response canReadBankActivationCode boolean indicates whether the caller may use bank codes.
filter (0–128 characters) searches name and authorized settings, or an exact bank KID/activation code. Optional bankType=Credit|Cash adds an exact type match to the text search, so localized clients can match their translated label without losing text matches. sort accepts name (default) or any optional field key; direction asc/desc. Stored settings sort as text; activation codes sort numerically with FlexActivation. Code sorting streams the authorized matches and retains only pageSize+1 rows; narrow the filter if the 12-second deadline is exceeded. pageSize 1–250, default 50. Keep all options unchanged with nextCursor (15-minute expiry, bound to manager and scope). Concurrent changes can move rows. Site-wide access may include all states; limited scope requires Enabled=1 and deletion within RetentionDays. enabledOnly=true and active-count require Enabled=1 and Deleted=0; missing Deleted means zero. No map. New optional fields are unreleased; clients are synchronized at beta.
GetBankAccountIcon: GET /api/v1/banks/{bankKid}/account/icon. Requires Account2 and Bank, Location, Unit and User Read. Counts all Log1 rows across periods in the authorized location scope, independent of amounts and current account filters. Trimmed three-letter codes are uppercased; invalid/blank codes are ignored. Highest row count wins; alphabetical currency breaks ties. Returns currency (null for empty data) and iconKid with the currency in its text. Render iconKid unchanged through GetIconFromSet.
GET /api/v1/banks?pageSize=50&sort=name&direction=asc&enabledOnly=true
Authorization: Bearer YOUR_MANAGER_TOKEN
GET /api/v1/banks/{bankKid}/account/icon
Authorization: Bearer YOUR_MANAGER_TOKEN
Use the existing manager bearer session. All reads are no-store. 400 means invalid input/cursor (restart pagination); 401 requires a new session; 403 requires the appropriate tab/read/scope grants; 503 is temporary storage failure (retry manually). These new endpoints are implemented locally and unreleased.
Resident directory — UserFinder1 (77)
GetUsers: GET /api/v1/users lists residents across the manager's authorized banks and locations. Requires UserFinder1, User Read and matching resource grants. The trusted site selects the tenant. Current Log7 data and RetentionDays determine visibility; location-limited managers see only associated residents. No legacy business tables are read.
GET /api/v1/users?pageSize=50&sort=identity&direction=asc&enabledOnly=true&filter=Alice
Authorization: Bearer {managerToken}
filter accepts 0–128 characters: literal name/number substrings, exact email/SMS, or a complete resident KID. sort is identity (default; numeric bank/user order), name or number; name/number sorting scans the full authorized candidate set and can be slower; direction is asc or desc. enabledOnly=true means not deleted (residents have no Enabled setting). Items contain kid, bankKid, name, number, iconKid and nullable deletedAt. Open an item using GetBankUsers with its bankKid and exact userKid; that single-resident read accepts UserFinder1. Bulk bank lists and all existing write/activation/balance operations keep their own Users2 and operation requirements.
Follow nextCursor with unchanged options and pageSize (1–250). Cursors expire in 15 minutes and are bound to manager, site, scope and query. A scanLimitReached page can be empty; continue when nextCursor is present. At most 1,000 candidates per call; no total count. Concurrent edits can move records between pages. Do not cache results or log personal search text. Handle 400 by restarting the query, 401 by renewing the session, 403 as a permission denial, and 503 with a user-initiated retry.
Administrator and installer counts
GetManagerCount: GET /api/v1/managers/count requires Managers1 (28), Managers Read and whole-tenant access. GetInstallerCount: GET /api/v1/installers/count requires Installers1 (68), Installer Read and whole-tenant access. Both recheck the active manager session and return {"count":123,"iconKid":"canonical icon KID"}. Counts cover all visible identities, independently of filters and paging, including disabled accounts and deletions within the caller's RetentionDays. Existing directory indexes may be 60 seconds old. The administrator index is bounded to 20,000 identities; installers to their 999-identity range. Exceeding a bound returns 503, never an estimate or partial count. No resident total is exposed.
401: renew the session; 403: check tab, Read and scope; 503: allow a manual retry. No-store. Fetch asynchronously when the navigation item becomes visible. Unreleased; generated clients follow at beta.
Service count
GetServiceCount: GET /api/v1/services/count returns count and iconKid for the unfiltered GetServices catalog. Same active session, Services1, Service Read and whole-tenant grant. Counts the same distinct concrete eUserId definitions as GetServices, excluding range-end markers; no service settings or extra business SQL is read. Existing session authorization still applies. 401: renew session; 403: check permissions; 503: retry manually. No-store. Unreleased.
Starts more than 100 hours before the measurement time are excluded; exactly 100 hours is included.
Running machines
GetPublicRunningMachines — GET /api/v1/public/statistics/running-machines
No login or per-machine permission is required; only a tenant-wide count is public. The site selects the tenant. Counts distinct bank/location/unit identities in Log24 using the latest Started and Done state values (UTC milliseconds since 2000-01-01): Started < now and Done > now. Missing, malformed or conflicting values do not qualify. This time-window estimate does not check online status. Both success and failure are cached for 15 seconds per API instance; poll every 15 seconds. HTTP 503 with Retry-After: 15 means unavailable, never zero. Reads are capped at 100000 lifecycle rows; exceeding that limit returns 503. The response contains tenantKid, count, measuredAtUtc and refreshAfterSeconds (15). No identifiers for individual machines are returned.
GetPublicReservations: GET /api/v1/public/statistics/reservations. No login or parameters; the site selects the tenant. Returns tenantKid, count, measuredAtUtc and refreshAfterSeconds (60).
Counts dated reservations that have not ended, including ongoing reservations. Uses the current tenant Log5 snapshot, whose primary key is start/bank/location/unit/type. Here Text (start minutes) and Amount (duration minutes) are signed SQL INT NOT NULL columns, read as Int32; digit and length validation for bank Log5.Text string storage does not apply. This shared table has no Period column: its trigger replaces a row only when the incoming MS2000 is newer, including the negative Amount of a cancellation. Period=0 filtering applies to bank Log5 history, not this snapshot. Amount must be positive and local start plus duration must be after now. Cancelled, ended, AUT/isy auxiliary entries and weekly templates are excluded. Location time zone inherits from the bank, defaulting to Europe/Copenhagen when absent. Only an aggregate is public, without identities or permissions.
Poll every 60 seconds. HTTP 503 means unavailable, never zero; retry after 60 seconds. Conflicting time-zone revisions, invalid time zones and more than 100000 candidate rows also return 503.
The display pool is a random sample of up to 1000 machines, started within 100 hours and ending within the next 3 hours. Larger eligible pools are sampled rather than rejected.
Numeric name placeholders such as [455] are resolved through Kombine.Flex.Localization using Accept-Language, after reading the shared raw cache.
Live machines
GetPublicLiveMachines: GET /api/v1/public/statistics/live-machines
Anonymous on every tenant site, using only its trusted configured tenant. The caller cannot select a tenant. Returns an array of unitKid, unitName, locationName, startedAtUtc, doneAtUtc and nullable temperature. Uses current Log24 Started < now < Done, eSetting.Name and eState.Temperature; no resident data. Missing or conflicting lifecycle values are excluded. Refresh every 60 seconds; select distinct random machines, retain running selections and count down locally each second. HTTP 503 means unavailable; retry after 60 seconds. Limited to 1000 running machines and 100000 source rows.
GET https://api.team.kombine.technology/api/v1/public/statistics/live-machines
Only currently running machines with a valid end time after now and no more than 3 hours ahead are eligible; stored Done values outside this window are excluded. The response schema still allows null for doneAtUtc; clients should not select entries without a valid end time in this window. Temperature is omitted visually when null. Up to 10000 additional metadata rows are read only for running machines and their locations.
ErrorUnit
Optional fields=errorUnit returns the last stored eState.ErrorUnit (331) as JSON text in items[].errorUnit. Empty string means absent; null means not selected. A recorded error may no longer be active. sort=errorUnit uses lexical stored-text ordering before pagination. Existing Units1, Bank/Location/Unit Read, scope and retention checks apply. Keep query options unchanged with nextCursor; 400: invalid fields/sort/cursor, 403: missing permission, 503: temporary unavailability. Unreleased; client packages are refreshed at beta.
GET /api/v1/units?fields=errorUnit&sort=errorUnit&direction=desc&pageSize=50
Authorization: Bearer YOUR_MANAGER_TOKEN
GetCameraNearestLocation — GET /api/v1/camera/nearest-location?latitude=55.7&longitude=12.5&accuracyMeters=5 selects the nearest enabled, retention-visible location within 100 metres. Coordinates are decimal degrees; accuracy is metres (0–100). Send a manager bearer token with Camera1 and Location Read/Write. Only trusted-tenant scoped locations qualify; bank/unit targets are excluded. The result uses CameraResourceResponse. 400: invalid position; 401/403: denied; 404: no eligible location; 409: the nearest two cannot be distinguished within twice GPS accuracy (minimum 1 metre); 503: timeout/storage failure or over 100 candidates. On 404/409, open the intended location manually. Normal prepare/upload authorization remains mandatory. No GPS position is persisted by this lookup.
GetRecentCameraPhotoMetadata — GET /api/v1/camera/recent/{id}/metadata reads the saved capture metadata for a recent public thumbnail. Copy the opaque id from the thumbnail URL returned by GetPublicRecentCameraPhotos and send a manager bearer token. Requires Camera1, resource Read, scope and retention visibility on every request; the public thumbnail token does not grant private access. Returns the same JSON metadata as GetCameraCaptureContent with part=metadata. 400 means an invalid or expired thumbnail token (refresh the public list); 401/403 means access denied; 404 means unavailable capture metadata; 503 means storage unavailable. Never expose this response publicly. The portal uses saved weather and orientation for hover information; missing indoor temperature is unknown, GPS travel direction is not camera heading.
GetCameraResource also returns requiresProximityCheck and nullable latitude/longitude in decimal degrees. Locations use their stored coordinates; units inherit their location coordinates. Banks return false and null coordinates. The portal warns before taking a photo or video when the measured distance exceeds 100 metres, or the distance is unknown (missing coordinates, denied GPS, a position older than 60 seconds, accuracy worse than 100 metres, or a failed lookup). Users can cancel or continue. Retrying an existing upload does not capture again. This advisory check does not grant or restrict API access.
Large JPEG metadata (unreleased fix): the original JPEG remains byte-for-byte unchanged. When raw metadata exceeds the 256 KiB duplication budget, or its JSON exceeds 512 KiB, embeddedMetadata.metadataExtraction is original-only: exif is null and attributes and embeddedBlocks are empty. embeddedBlockReferences retains each JPEG metadata payload as Kind, Offset and Length; offsets are zero-based byte positions in the original file and exclude marker and length bytes. Read that authorized original to recover the full metadata. Smaller JPEGs retain extracted metadata and report complete. The 32 MiB photo limit, pixel and segment-count limits, structural validation, and compressed PNG metadata limits still apply.
More specific JPEG diagnostics (unreleased): photo-pixel-limit, photo-metadata-limit, photo-metadata-block-limit, photo-jpeg-marker, photo-jpeg-truncated-marker, photo-jpeg-missing-frame, photo-jpeg-structure, photo-jpeg-truncated-segment, photo-jpeg-encoding, photo-jpeg-frame and photo-jpeg-missing-end. These distinguish resource limits from malformed or unsupported JPEG data; the HTTP status and error code remain unchanged.
Photo rejection diagnostics (unreleased): HTTP 422 retains invalid-camera-media. An optional ProblemDetails diagnostic identifies the failing stage: photo-size, photo-format, photo-jpeg-header, photo-png-header, photo-decode, photo-metadata or photo-thumbnail. It contains no image metadata or decoder messages; clients must tolerate a missing or unknown value.
Private camera captures (unreleased)
Camera1 uses the current manager's tenant, tab and resource permissions. Listing and media require resource Read; preparation and upload also require Write. The tenant list contains the manager's own captures; a resource list includes other authorized managers' captures on that exact bank, location or unit. Service accounts are not accepted.
GetCameraResource: GET /api/v1/camera/resource?resourceKid=KID returns resourceKid, name and iconKid for the exact authorized bank, location or unit. Use this for the local camera heading instead of loading a parent overview or child directory. It requires Camera1, that resource type's Read permission, scope and retention visibility; a location does not require Unit Read and a unit does not require Location Read. Units include only the inherited location coordinates, not other parent or child metadata. Pass iconKid unchanged to the icon API. Errors: 400 invalid/cross-tenant KID, 401 invalid session, 403 denied, 404 missing or outside retention, 503 unavailable lookup.
GET /api/v1/camera/resource?resourceKid=CANONICAL_RESOURCE_KID HTTP/1.1
Authorization: Bearer YOUR_MANAGER_TOKEN
GetCameraAvailability: GET /api/v1/camera/availability?resourceKid=KID returns hasCaptures, isDegraded and canCapture for lazy navigation. GetCameraCaptures: GET /api/v1/camera?resourceKid=KID&cursor=TOKEN returns items, nextCursor and isDegraded. Omit resourceKid for own captures. Follow the opaque nextCursor with the same scope, including empty filtered pages. Both private regional indexes are queried and deduplicated; degraded results may be incomplete and must be refreshed from the beginning after recovery.
GetCameraCaptures items also include nullable bankName, locationName and unitName (unreleased). These are current display names, not names frozen at capture time. Each name requires that object's Read permission, resource scope and retention visibility; inapplicable or unauthorized names are null. Unit names resolve localized placeholders. Clients may display the available names in bank/location/unit order and must continue using resourceKid for identity and navigation.
Browser file imports use the same prepare/upload operations and permissions. capturedAtUtc accepts dates from 1970-01-01 UTC through five minutes ahead of server time (unreleased). When the capture time is unknown, clients may use file last-modified time or upload time and declare the source in sensorData.timestampSource. Do not attach current device sensors to historical media. The portal file picker supports JPEG/PNG up to 32 MiB and MP4/MOV/WebM up to 128 MiB; the server validates the original media.
For file imports, resolve the target with GetCameraNearestLocation using embedded EXIF GPS (JPEG/PNG) or QuickTime location metadata (MOV/MP4), not the uploading device’s current position. The portal uses a conservative 100-metre accuracy window when metadata does not specify accuracy; missing, invalid or ambiguous geodata stops the import without uploading. Files retain their original bytes. The separate /camera/upload page does not open the camera or microphone.
When upload is opened from a location or unit, the portal uses that explicitly selected, API-authorized resource. GPS is optional in this case. A distance greater than 100 metres from its location coordinates produces a confirmation warning; declining skips only that file. Tenant-level uploads still resolve their target from embedded GPS.
PrepareCameraCapture: POST /api/v1/camera/prepare accepts the JSON below. Hash the original bytes with SHA-256. Retain the returned uploadToken for retries: it binds the authenticated owner, resource, original hash and metadata, including optional weather, for 24 hours. SaveCameraCapture: POST /api/v1/camera accepts multipart uploadToken and original (binary file). Success confirms media, generated thumbnail and matching JSON in one region; the second copy continues independently of navigation. There is no success notification requirement.
After an uncertain upload response (for example HTTP 504), call ConfirmCameraCapture with the original ticket. This operation requires the same authenticated owner and current Camera1/resource Read and Write. Only confirmed: true confirms publication of the exact capture; false or a 503 response does not prove failure. Keep the original bytes and ticket for a later retry; do not create a new plan automatically. Invalid/expired tickets return 400; missing authentication returns 401 and revoked access returns 403/404. This confirmation operation is unreleased; generated clients will be refreshed at the next beta release.
Optional ?background=true on SaveCameraCapture returns HTTP 202 after validating and staging the original bytes. This is not a durable save acknowledgment. Poll ConfirmCameraCapture with short requests until confirmed=true. Processing continues independently of the upload connection. The bounded processing queue is temporary: after restart or an unconfirmed result, retain the original and retry the same ticket. A confirmation errorCode of camera-processing-failed indicates that this instance stopped processing; retry is permitted. Default requests retain the synchronous 200 behavior.
The optional processingPercent field is a weighted estimate based on completed processing stages, not elapsed time or a time-remaining prediction. Values 0–99 represent temporary progress on the processing instance; null means unknown (for example another instance or a restart). Only confirmed=true returns 100. Clients must never infer successful storage from a progress value alone.
POST /api/v1/camera/confirmation
Authorization: Bearer YOUR_MANAGER_TOKEN
Content-Type: application/json
{"uploadToken":"ORIGINAL_UPLOAD_TOKEN"}
POST /api/v1/camera/prepare
Authorization: Bearer YOUR_MANAGER_TOKEN
Content-Type: application/json
{"resourceKid":"CANONICAL_RESOURCE_KID","kind":"photo","originalFileName":"IMG_1234.jpg","originalNameAvailable":true,"capturedAtUtc":"2026-10-15T08:00:00Z","mediaLength":12345,"mediaSha256":"SHA256_OF_ORIGINAL_BYTES","sensorData":{}}
GetCameraCaptureContent: GET /api/v1/camera/{id}/content?part=thumbnail streams through the authorized API. Obtain id from the list; identities expire after 30 minutes. Parts: original, metadata, thumbnail, preview, motion, motion-playback and playback when present. List items expose hasPreview, hasMotion and hasPlayback. For a video with hasPlayback=true, use part=playback; otherwise use original. Live Photos use original for the still and motion-playback for motion; motion retains the original MOV. Single byte ranges are supported for playback. No Spaces credentials, direct or signed storage URLs are returned.
Current capture support: JPEG/PNG photos up to 32 MiB and MP4/WebM/QuickTime MOV videos with audio up to 128 MiB, with a 60-second browser stop and a 61-second server ceiling for recorder completion. Each preview is 512 square; video previews contain three silent frames. Native live-photo uploads use a JPEG still (32 MiB maximum) plus its original QuickTime MOV companion (32 MiB maximum, at most 10 seconds). Prepare with kind=live-photo and additional motionLength and motionSha256 fields; upload both original and motion multipart files with the same uploadToken. Both components are hashed and decoded before publication. The thumbnail is static; no animated preview is generated for Live Photos. Ordinary QuickTime MOV and WebM videos retain their original bytes and have a separate H.264/AAC MP4 playback derivative for clients that cannot play those formats directly. Live Photo motion likewise keeps its original MOV and has a separate MP4 derivative, without invented audio when the motion original is silent. The complete regional set includes this derivative before publication. Native capture still requires platform/device verification; Android OEM Motion Photo integration is not claimed. Video requires configured ffmpeg/ffprobe on the API host. Original bytes are preserved. New capture filenames use a GUID and the media extension, without region labels or the device filename (unreleased). Related files retain suffixes such as .thumbnail.jpg and .json. The original device filename and origin remain in JSON metadata. Existing captures keep their names and remain readable. Treat returned filenames as opaque; the filename GUID is not the capture ID. Copies keep their origin name. Conditional object creation prevents overwriting distinct bytes; no camera database is used.
400: malformed KID, token, metadata or hash; 401/403: session/access; 404: missing resource/component; 413: size limit; 416: invalid range; 422: unsupported/invalid media; 429: capture processing busy; 503: storage or required processor unavailable. Retry the same capture with its retained uploadToken and original bytes. Missing sensors/weather remain explicitly unavailable and do not invent measurements. Interrupted replication and rare competing origin manifests for the same retried capture require later reconciliation; one surviving copy remains readable. No deletion, gallery import or automatic historical-weather repair is implemented.
AutoOutOfOrder
Optional autoOutOfOrderOnly=true filters units by valid Log24 AutoOutOfOrder JSON with a positive integer Id, independently of the OutOfOrder setting. The optional response field autoOutOfOrder contains that JSON when filtering, otherwise null. Follow nextCursor with the same filter to read all matches; each page rechecks Units1, Bank/Location/Unit Read, scope and RetentionDays. 400: invalid or changed cursor; 401/403: authentication/access; 503: storage unavailable. Unreleased; client packages follow at beta preparation.
GET /api/v1/units?autoOutOfOrderOnly=true&sort=kid&fields=bankName,locationName
Authorization: Bearer YOUR_TOKEN
GetPublicMachineErrors
No login or bearer token is required. The site selects the tenant. Returns items (kid, name, bankName, locationName, errorId as a digit string), nextCursor and refreshAfterSeconds=60. Only positive AutoOutOfOrder IDs whose Log24 MS2000 event time is within the last 10 days (inclusive) are included. Unknown and future timestamps are excluded. Deleted banks/locations/units and disabled locations are excluded; disabled units remain included. No raw error payload is public. pageSize=1–100 (default 50); follow nextCursor unchanged with the same pageSize. The portal loads every page automatically and fits all icons into its card. Cursors expire after 15 minutes and cannot cross tenants. 400 invalid page/cursor; 503 unavailable (Retry-After: 60), never an empty success. Unreleased.
GET /api/v1/public/widgets/machine-errors?pageSize=100
Purchase map widget
Mount the compact live purchase map directly, inside its own Shadow DOM. No iframe, portal shell, login or bearer token is required. The API site selects the tenant. Data refreshes every 10 seconds; failures back off for 30 seconds. Call destroy() when removing the widget. Supply googleMapsBrowserKey from the embedding website's public browser-key configuration; never use a server geocoding key. The option takes precedence over the nullable googleMapsBrowserKey from GetPublicWidgetPresentation (available only when the API already has GoogleMaps:ApiKey configured). The API does not need that setting when the embedding site supplies its own key. Without either source, ready rejects. Google HTTP-referrer restrictions must allow the embedding site; the widget does not change them. For sites with Content Security Policy, also follow Google Maps CSP guidance.
After widget.ready, widget.getView() returns the current { startPosition: { latitude, longitude }, zoom }, or null when unavailable or destroyed. It reflects manual map movements. Unreleased.
Set startPosition: { latitude: 56.2, longitude: 10.5 } to choose the initial map centre. Latitude must be from -90 to 90 and longitude from -180 to 180. Combine with zoom: 6. With startPosition, automatic data fitting is skipped; live purchase following and manual navigation remain available. Omit it to keep automatic fitting. Available in source; unreleased.
Map layers: showCoins defaults to true; showHeatmap defaults to false. For a heatmap-only card, set showCoins: false, showHeatmap: true. Available in source; unreleased.
Set zoom: 7 to use a fixed zoom level after centering on the data. Values from 0 to 22 are supported. Omit zoom to fit all locations automatically. Unreleased.
Latest photo widget
Unreleased. GetPublicRecentCameraPhotos: GET /api/v1/public/camera/photos returns up to ten photos (including Live Photo stills), newest capture first, across the configured tenant. No login or tenant input. Each item contains thumbnailUrl and workspacePath. Fetch thumbnailUrl unchanged (GetPublicCameraThumbnail); tickets expire after ten minutes and only allow JPEG thumbnails. Original media, owner details and sensor metadata remain private. Opening workspacePath requires an active manager with Camera1, resource Read, scope and retention visibility. The overview shows each image for three seconds (component parameter DisplaySeconds, 1–60), with a 500 ms fade, pause control and reduced-motion support. Refresh the list every minute; do not persist tickets. Empty array means no photos. 400: invalid/expired/cross-tenant ticket; 404: missing thumbnail; 503: unavailable or incomplete storage snapshot. Retry after one minute. Both regional indexes must be available; scans are bounded to 10,000 index entries and 200 candidate manifests. Videos are excluded; no database writes or direct storage URLs.
GET /api/v1/public/camera/photos
Manager API keys (unreleased)
In Portal, open My settings and choose Generate key. GetMyManagerApiKey — GET /api/v1/session/me/api-key returns hasApiKey and revision. GenerateMyManagerApiKey — POST /api/v1/session/me/api-key accepts {"revision":"REVISION_FROM_GET"} and returns the one-time apiKey plus details with the new status/revision. An active tenant-bound manager can generate only their own key, without administrative permissions. Copy the plaintext immediately; it cannot be recovered. Never log this response.
Both Portal LoginManager and Equipment LoginService accept {"kid":"CANONICAL_MANAGER_KID","apiKey":"YOUR_KEY"} at POST /api/v1/session/login. Email/password remains supported; do not mix pairs. Choose the correct tenant host; tokens are isolated between APIs and environments. KID login does no duplicate-account cleanup. The token retains the manager's existing Tab/KID/operation permissions and three-day lifetime. It never grants service synchronization rights. Renew after activity; replacement of the key revokes key sessions within 60 seconds, including renewed sessions.
Manager and service generation store only the password-compatible hash in eSetting.ApiKeyHash, preserving Password. For services, ApiKeyHash is authoritative when present; an explicitly empty value disables the key. Existing service keys stored only in Password remain readable until replaced. Never copy manager passwords into ApiKeyHash. Handle 400 for invalid/mixed input, 401 for invalid key or inactive manager, 409 for concurrent generation (reread the status), and 503 for unavailable or uncertain storage. Never retry generation automatically.
API-key login username
LoginManager · POST /api/v1/session/login — Unreleased. For manager API-key login, send the username shown in My settings in the kid field: Kid.ToUserId format, tenant.0-userId (for example 166.0-1000002213). Send apiKey with the original key. The canonical manager KID is also accepted. The tenant must match the configured API host; only bank-zero managers are accepted. Resource IDs and response KIDs remain canonical. Do not mix kid/apiKey with email/password.
Nearest accessible locations
GetNearestLocations — GET /api/v1/locations/nearest?latitude=55.6761&longitude=12.5683 returns up to ten enabled locations as an array of {kid,name,bankName,iconKid,distanceMeters}, ordered by straight-line distance in metres. Supply both finite coordinates in decimal degrees (latitude −90…90, longitude −180…180). No search filter or radius applies; locations without valid nonzero coordinates are omitted. Requires an active manager, Locations1, Bank Read, Location Read and matching KID scope; retention visibility is preserved. No extra permissions are granted by a position. Errors: 400 invalid-position, 401 invalid session, 403 missing-locations-access, 503 locations-unavailable. An empty array means no eligible locations. Responses are no-store. Positions are used only for this lookup; do not log query strings or persist them. The portal retrieves the browser position automatically when the card appears. The browser may request location permission; refusal or unavailable positioning produces a localized message.
Reservation history
GetLocationReservationHistory — GET /api/v1/locations/{locationKid}/reservation-history Each item also includes canonical userKid (null for an unknown resident). For display only, fall back to userKid when userNumber is empty; userName remains empty when unavailable.
Load history for the visible calendar range only. Requires Bookings1, Location Read, Unit Read, matching resource scope and retention visibility. Returns bank Log5 revisions for visible units with userNumber and userName from bank Log7 (empty strings when missing); User Read is required. From is a local date from 2001; days is 1–31. Each item contains unitKid, startMinute (local minutes since 2000-01-01), durationMinutes (positive absolute duration), revision (UTC milliseconds since 2000-01-01) and cancelled. Superseded and cancelled events remain in history; weekly/AUT/isy events are omitted. History does not describe current occupancy. On 400 correct the range/KID, on 401 sign in again, on 403 check permissions, on 404 refresh the location, and on 503 retry later and show history as unavailable. The 5000-event limit fails with 503 instead of truncating. Unreleased.
GET /api/v1/locations/LOCATION_KID/reservation-history?from=2026-10-20&days=7
Authorization: Bearer YOUR_MANAGER_TOKEN
The event matching a current active reservation by MS2000, LocationId, UnitId and UserId in the same trusted tenant and bank is omitted from history.
Unit WashDoc lists and their separate count accept optional search: an exact numeric Doc ID or canonical document KID belonging to the selected unit. The inclusive date cutoff still applies. Invalid or other-unit identities return HTTP 400 invalid-document-search. Clear search to restore unfiltered history. Routed API responses may include X-Portal-Operation, the stable Swagger operation name, for contextual documentation; it contains no request values.