Zenith · Integration Hub

The contract between their fields and ours.

Every integration is a mapping you design against a real record pasted from the other system — and a dry run that names four things before you trust it: what would land, what is missing, what they send that nothing reads, and which transforms fail on the actual value. “Silently dropped” is the sentence this screen exists to delete.

Bilingual EN / FR · Published API contract · WCAG 2.2 AA
4 named lists
Landed, missing, unread and failing — recomputed on every keystroke
19 endpoints
A published API contract, with the client generated from it so the two cannot drift
Inbound only
Readings come in. Nothing here sends a command or a setpoint back to a device
EN · FR
Every label, hint, empty state and refusal, in both languages
The contract

Six things make an integration

An integration here is not a switch you turn on and hope about. It is a contract between somebody else's fields and yours — written down, checked against evidence, and refused until it holds.

01

An entity

Vendors, parts, assets or work orders. Their record on one side, ours on the other, and the contract in between.

02

A direction

Inbound or outbound, recorded on the contract and shown on the register beside the system it talks to.

03

A real sample

One actual record, pasted from the other system. It is the dry run's food — everything below is computed from exactly this.

04

Field mappings

Their field as it appears in the sample, our field chosen from what the entity offers, and one of five transforms applied live.

05

Required fields, marked

An inbound vendor without a name is not a vendor, it is a row of noise — so the required ones carry a badge on the row and a refusal at the gate.

06

A state

Draft until every named blocker is cleared. Enabling is gated on exactly the arithmetic the dry-run panel is already showing you.

The dry run

Four lists, and none of them is optional

Paste one real record and the panel answers immediately: what would land, with the value the transform actually produced; what is mapped but absent from the sample; what the other side sends that nothing on your side reads; and which transforms failed on the real value rather than the type they were promised.

  • The value, not the transform's name — “currency = CAD ← WAERS” shows what uppercase did to the actual string, so a wrong transform is visible rather than plausible.
  • Mapped but missing is reported, never skipped — a mapping reading a field they do not send is a column of blanks in production, discovered a month later.
  • What they send that nothing reads is named — this is the list that catches the field somebody added to the export last quarter and told nobody about.
  • Failing transforms carry the value"not recorded" is not a date, rather than “type mismatch on field 7”.
  • It recomputes on every keystroke — edit the sample or change a transform and the four lists move under your hand, so it fails here instead of in production.
The gate

Draft until every named blocker is cleared

A contract you cannot check is a contract you find out about in production, usually from somebody in another department. So enabling is refused until the dry run comes back clean, and every refusal names its own field rather than announcing that something, somewhere, is invalid.

  • The sample must be JSON — otherwise nothing can be checked against it, and the panel says exactly that rather than showing four empty lists.
  • Every required field must be mapped — by name, so the fix is obvious rather than a hunt through a schema.
  • Every mapping must find its field — in the sample you pasted, which is the only evidence anybody has.
  • Every transform must survive the real value — the one that actually arrived, not the one the specification promised.
  • Unread fields never block — what they send and nobody consumes is information worth having, not an error worth stopping for.
Underneath it

A published contract, and a client generated from it

Beneath the mapping designer there is a real API: a typed client built from an OpenAPI document, speaking the same envelope the product already uses internally. Moving a screen onto the server changes where truth lives, not how the screen asks for it.

One envelope

A refusal over the wire looks like a refusal in memory

A success comes back as a value; a refusal comes back as a reason and the fields it names — and a 422 from the server surfaces as the same typed result an internal command returns. That is what lets a screen move onto the server without being rewritten around a new error convention.

  • A 401 arrives in the same envelope — so the shell can tell “your session ended” from “something broke”, which are different sentences to show somebody.
  • The server being away is a state, not an exception — “unreachable” is a reason like any other, and the console renders it as one instead of a stack trace.
  • The token is read fresh on every request — a client holding the value it was built with keeps sending a token that was revoked ten minutes ago.
  • Transport is injected, not imported — tests hand it a fake and the browser hands it the real thing, which is also what keeps it usable from a script or a field app.
  • Blockers come back as a list — the accept endpoint refuses with the same blockers the register enforces, so the API and the screen cannot disagree about why.
Deployment

Re-point a live site without a rebuild

Where the interface believes the server lives is decided in three places, in order — so a deployed site can be pointed at a different API by editing one file, rather than rebuilding and redeploying the whole bundle to change a URL.

  • This browser's own choice first — set on the server console for one developer against one branch, and never seeded into anybody else's session.
  • Then a config file beside the page — the one that matters in production: a static site is re-pointed by editing a single file in storage.
  • Then a value baked in at build time — for deployments that would rather pin it than ship a config file.
  • The development default is localhost, on purpose — a deployed site with no configuration must fail to find an API rather than accidentally find somebody else's.
  • Identity has its own address — sign-in and the register API are different services, and sharing one base URL would point the login screen at a server with no login endpoints.
Meter sources

Readings come in. Nothing goes back out.

A point from a building-automation system or a controller, mapped onto a meter, with a linear transform you can read on the row. The boundary is deliberate, and it is printed on the screen rather than buried in a datasheet: readings come into Zenith, and nothing here sends a command or a setpoint back to a device.

The safe boundary

A device reading obeys every rule a typed one does

Readings arrive through the meter register's own command — the same door a person typing on a round goes through. So a reading that runs backwards on a cumulative meter, or arrives implausibly large, is refused by the register rather than waved through because a machine sent it.

  • The transform is on the row, not in a stored procedure — the panel reports kPa and the meter is in Pa, so the scale is 1000 and you can see it, argue with it, and change it.
  • Refused attempts are kept, loudest — with the reason the register gave, in the register's own words.
  • Inbound only, and the screen says so — a badge on the page, because a boundary nobody can see is a boundary somebody will assume away.
  • The run log is already the door — the same log a real dispatcher will write through, so history does not restart the day transport lands.
Where the line is

What is designed, and what is the server's half

An integration module is the easiest thing in a maintenance system to sell on a diagram. This one draws the line where its own code draws it — and prints it at the foot of the register rather than leaving you to find out in month four.

  • Transport names are recorded, and honest about it — REST, file drop and webhook sit on the contract; the dispatcher that speaks them is the server's half.
  • The API underneath is not a mock — a real typed client over a published contract, already carrying sign-in, sessions and refusals.
  • Nothing is dressed up as connected — a screen that has not moved onto the server says so, on the screen.
  • The contract is the part that has to be right first — a schedule pointed at a mapping nobody checked just fails faster and more often.
And the rest of it

Everything around the mapping

Five transforms

As-is, uppercase, lowercase, date to ISO, and number — each applied to the sample live, so the one you picked is judged on the value that actually arrived.

Twenty target fields

Five for each entity, with the non-negotiable ones badged on their own row rather than discovered at the gate.

One query contract

Search, filters, sort and paging as a single shared document — the same one the registers already speak, so a screen asks the server exactly what it asked memory.

Edits apply in place

Change a transform and the dry run updates underneath it. There is no save button to forget and no draft state to reconcile.

A health check with an answer

It reports which identity store replied — the live one or the seed — so “am I looking at production?” stops being a guess in a support call.

Generated, not hand-written

The client comes from the OpenAPI document by a generator, so the published contract and the code that calls it cannot quietly drift apart.

Sign-in that survives a change

Sessions, groups, site membership and a forced password change all arrive through the same client — with the token re-read on every call rather than captured once.

Your words, everywhere

Direction, entity and status all read through your own vocabulary, so a contract describes itself in the terms your organization already uses.

Two languages, all the way down

Every label, hint, empty state and blocker exists in English and French — including the sentence that tells you a contract cannot be enabled yet.

Enterprise foundations

Checkable before anything moves

Integrations fail quietly, months after anybody was watching, and the post-mortem is always the same: a field nobody mapped, a value nobody validated, a message nobody kept. Everything here is built against those three specifically.

Evidence, not assumption

A contract is judged against one real record you pasted, not against a specification somebody wrote about the record.

One envelope, either side

A refusal in memory and a refusal over the wire have the same shape, so nothing is rewritten as a screen moves onto the server.

Bilingual by birth

Full en-CA / fr-CA parity on every screen, hint, empty state and refusal — plurals handled properly, not by concatenation.

Accessible to everyone

WCAG 2.2 AA as a build rule: real table semantics, states announced in words, and focus that moves to the field an error refers to.

Built on OpenAPI contract · v0.1.0 Bearer authentication WCAG 2.2 AA
Questions people actually ask

Before you book the demo

Do you have a SAP connector?
Not as a pre-built, logo-on-a-slide connector, and we would rather say that than sell one. What you get is a mapping contract designed against a real SAP record — their field names, your fields, the transforms in between — dry-run against that record until every blocker is cleared. The system name on a contract is text somebody types. The transport that carries the records is the server's half, and it is the same half for every system rather than a different integration per vendor.
Is there a real API, or just mappings?
There is a real one, and it is the part worth looking at. Nineteen endpoints published as an OpenAPI document, with the typed client generated from it so the two cannot drift; bearer authentication with the token re-read on every request; sign-in, sessions, groups and site membership; and refusals that arrive in the same envelope an internal command returns. Assets, service requests and work orders are on it today.
What formats can you accept?
A contract records whether it is meant for REST, a file drop or a webhook. The dry run works on one JSON record at a time — the sample you paste — because that is what makes it readable. Batch files and arrays are not dry-runnable today; they arrive with the transport half, and they will be checked by the same mapping the single record was.
Can I map nested fields, or arrays?
No — top-level keys only today. A mapping reads one field of the pasted record by name. Nested paths like customer.address.city and repeating groups are not supported, and a payload that is an array of records cannot be dry-run at all. If your export nests, the honest answer for now is to flatten it on the way out.
Are there default values, lookup tables or custom transforms?
No, and the smallness is deliberate. Five transforms, one payload key each: no constants, no concatenation, no conditional logic, and no value maps translating their CLSD to your Closed. A mapping layer with an expression language is one nobody can read at three in the morning — and the dry run only stays legible because every row is one field and one rule. Value translation is a fair ask and it is on the list.
What happens when the server is unreachable?
It is treated as a state, not an exception. The client returns “unreachable” as a reason like any other and the console page renders it as a condition of the system, so an API that is down looks like an API that is down — rather than a stack trace, a spinner that never resolves, or a screen that silently shows yesterday.
Can a deployed site be pointed at a different API?
Yes, by editing one file. The address is resolved in three places in order: a choice made in that browser, a config file sitting beside the page, then a value baked in at build time. So a static deployment moves between environments without a rebuild — and the development default is deliberately localhost, so a site with no configuration fails to find an API rather than accidentally finding someone else's.
Do you write back to a BMS, or send setpoints?
No. Readings come in; nothing sends a command or a setpoint back to a device. That boundary is stated on the meter-sources screen itself, with a badge, because a maintenance system that can write to plant equipment is a different product with different consequences — and a boundary nobody can see is one somebody eventually assumes away.
Do device readings skip the usual validation?
They do not. A mapped reading is stored through the meter register's own command, the same door a technician typing on a round goes through — so a reading that runs backwards on a cumulative meter, or is implausibly large for the elapsed time, is refused by the register rather than trusted because a machine sent it. The refusal is kept in the run log with the register's own words.
What about scheduling, retries and run history?
Those are the server's half, and the register says so at the foot of the screen rather than in a footnote. What exists today is the run log itself — the same log the real dispatcher will write through when it lands, so the history does not restart on the day transport arrives. The contract is the part that has to be right first: a schedule pointed at a mapping nobody checked simply fails faster and more often.
Is a contract versioned or audited?
Not today — a change to a mapping applies in place, with no revision history, no diff and no record of who changed it. That is a real gap and worth knowing before you plan around it. Versioning and audit belong with identity and history, which are the server's to own; the workflow module already does exactly this for its own definitions, and the same treatment is the intended destination here.
How would we start?
With one record. Export a single vendor row, a work order, or one line of a controller's point list, and we will build the contract in front of you — mapping their names to ours, watching the four lists move as we go. The most useful thing that happens in that session is usually the third list: the fields their system has been sending for years that nothing at your end has ever read.
See it on a record you already have

Bring one row from the other system.

A single vendor from SAP, one work order export, a line from a BAS point list. Paste it in and we'll map it in front of you — and the dry run will tell you on the spot which of their fields nothing at your end reads, and which of yours nothing supplies.

Zenith · Integration Hub Part of the Zenith maintenance & asset operations platform